제미로그 | JPA를 활용한 API 개발에서 배운 내용 정리하기
2025. 8. 1. 07:04ㆍProjects/JemiLog
[API 개발]
1. API URL 및 HTTP 메서드 설계
- RESTful API는 리소스 중심(명사) + 행위는 HTTP 메서드로 구분
- 목록 조회: GET + /api/tags
- 단건 조회: GET + /api/tags/{id}
- 등록: POST + /api/tags
- 수정: PUT + /api/tags/{id}
- 삭제: DELETE + /api/tags/{id}
- URL의 식별자(id)는 @PathVariable, 요청 본문은 @RequestBody로 받는다.
- 상태코드는 ResponseEntity를 통해 명확히 반환한다.
2. Controller 메서드 시그니처 구성 규칙
@PutMapping("/{tagId}")
public ApiResponse<Void> updateTag(
@PathVariable Long tagId,
@RequestBody TagUpdateRequest request
)
- URL 경로 ID → @PathVariable
- 등록/수정 데이터 → @RequestBody
- 쿼리 파라미터 → @RequestParam으로 받고 null 체크로 분기
3. API 응답 구조 통일: ApiResponse<T>
public class ApiResponse<T> {
private boolean success;
private T data;
private String message;
public static <T> ApiResponse<T> ok(T data) {...}
public static ApiResponse<Void> fail(String message) {...}
}
- 일관된 포맷으로 응답 → 클라이언트 처리와 디버깅 편의성 ↑
- 단순 메시지는 SimpleMessageResponse 같은 별도 DTO로 처리 가능
4. DTO 설계 및 구성 방식
- 실무에서는 도메인 객체(엔티티)와 API 요청/응답을 분리하기 위해 DTO를 사용한다.
- Entity를 직접 노출하면 보안/유지보수 측면에서 위험함 (불필요한 정보 노출, 구조 변경 시 영향 등).
- 요청/응답 요구사항은 Entity와 다를 수 있기 때문에 별도의 구조가 필요함.
- controller와 DTO는 도메인 하위 패키지에 함께 두고, dto/ 폴더를 만들어 구분하면 관리와 확장성이 좋다
- DTO 네이밍
- 요청용 DTO는 CreateXXXRequest, UpdateXXXRequest 등으로, 응답용 DTO는 XXXResponse로 이름을 구분한다.
- Dto 접미사는 명확한 의미 전달이 필요한 경우에만 사용하며, 생략하는 것도 가능하다.
목적 | DTO 네이밍 | 설명 |
생성용 | TagCreateRequest | 생성에 필요한 필드만 담기 |
수정용 | TagUpdateRequest | 수정 가능한 필드만 담기 |
조회용 | TagResponse | Entity의 값을 응답용으로 가공 |
상세 조회용 | TagDetailResponse | 필요한 필드만 뽑아서 클라이언트에 보여줄 용도 |
목록용 | TagListResponse + TagSummaryDto | 여러 개일 때 응답 구조화 |
- DTO 변환 메서드 네이밍 규칙
변환 상황 | 메서드명 | 예시 |
DTO → Entity 변환 | of() | Tag.of(TagDTO); |
Entity → DTO 변환 | from() | TagDTO.from(Tag); |
5. Service/Controller/Repository 네이밍 컨벤션
- Service/Controller 메서드: 동사 + 도메인명
예: createTag(), getTags(), updateTheme() - Repository 메서드: Spring Data JPA 규칙
예: findById(), deleteByName()
6. Service 계층의 역할과 구조
- 복잡한 비즈니스 로직, 연관관계 처리, DTO 변환 등을 책임짐
- 단순 CRUD도 DTO 변환 책임은 Service에 둔다
- 여러 Service 간 의존이 있어도, 비즈니스 상 의미가 있다면 괜찮다. 단, 역할과 책임은 명확히 분리할 것.
- 트랜잭션은 읽기 전용(@Transactional(readOnly = true))과
쓰기(@Transactional) 로직에 따라 구분해서 사용하는 것이 효율적이다.
7. 컬렉션 포함 응답 구조
- 예: ThemeResponse에 포함된 List<TagResponse>
- 용도에 따라 포함 여부 결정 (ex. 미리보기 vs 상세 조회)
- 목록 응답은 페이징을 고려해 ThemeListResponse 형태로 감싸는 것이 좋음
8. JPA 관련 API 설계에 영향 주는 부분
1) Builder와 생성자 사용 시 주의
- Builder 사용 시 컬렉션 초기화가 안 되므로 주의
- 생성자에서 직접 초기화하거나, 필드 선언 시 new ArrayList<>()로 초기화 필요
2) 연관관계 편의 메서드 분리
- 중간 엔티티 사용 시, 연관관계 로직은 도메인 내부가 아닌 별도 서비스에 위임
- 예: TagThemeService에서 Theme–Tag 연관 처리
[JPA 개발]
1. 엔티티 설계와 객체지향적 구조
- 엔티티 생성은 생성자와 update() 메서드로 역할을 분리한다.
→ 생성자는 초기값 설정, update는 이후 상태 변경 담당 - setter 사용은 지양하고, 의미 있는 메서드명으로 필드 변경을 캡슐화한다.
- @NoArgsConstructor(access = PROTECTED)를 붙여 JPA 프록시 생성을 허용한다.
- BaseEntity는 @MappedSuperclass로 설계하고, @PrePersist, @PreUpdate를 통해 생성일/수정일 자동 관리 가능.
- boolean 필드의 경우 isDeleted()가 자동 생성되지 않으므로, 필드명을 deleted로 하거나 직접 구현한다.
2. 연관관계 매핑과 관리 전략
- 다대다 관계는 반드시 중간 엔티티를 만들어서 처리한다. (예: TagTheme)
- 중간 엔티티도 JPA의 엔티티로 정의해야 하며, 연관관계 편의 메서드를 통해 연계 상태를 명확히 관리한다.
- 연관관계 설정은 중간 엔티티 내부에서 직접하지 않고, 외부에서 static factory method로 생성과 연결을 함께 처리한다.
- 양방향 연관관계는 꼭 필요한 경우에만 설정하고, 양쪽 컬렉션을 모두 관리해야 객체 상태가 일관된다.
- 연관관계를 정리할 때는 삭제 대상을 복사한 후 for-each로 remove, delete 처리한다.
- 단방향 연관관계는 Missionlog, Recommend처럼 단순 참조 관계에서 사용하며, 조회 시 repository를 직접 활용하는 것이 실용적이다.
3. 영속성 관리와 저장 전략
- cascade = CascadeType.ALL을 설정하면 연관된 엔티티도 함께 persist()됨.
- 단, 연관 객체가 영속 상태가 아니면, 먼저 persist()해야 오류가 발생하지 않음.
- flush()와 clear()를 활용해 영속성 컨텍스트를 초기화하고, DB에 반영된 데이터를 테스트로 검증할 수 있음.
- 더티 체킹(dirty checking)은 영속 상태의 엔티티에서 필드 값이 바뀌면 트랜잭션 커밋 시 자동으로 update 쿼리가 나감.
- @Column(updatable = false)는 해당 필드를 더티 체킹 대상에서 제외시킴.
→ 주의: 이후 수정할 가능성이 있는 필드(ex. isDeleted)에는 사용하지 말아야 한다.
4. soft delete 구현 전략
- 실제 삭제 대신 is_deleted(lombok 사용시 deleted) 같은 boolean 필드를 사용해 논리 삭제 구현
- 필터링 유지 방법
- Hibernate 6.3 이상에서는 @SQLRestriction 사용
- 단, @MappedSuperclass에는 적용되지 않음
- 또는 모든 쿼리에 수동으로 조건 명시
- @SQLDelete를 함께 사용하면 DELETE 호출 시 UPDATE is_deleted = true가 자동 실행됨
- soft delete는 cascade에 영향을 주지 않음 → 연관 객체도 직접 삭제하거나 상태를 조정해야 함
- 중간 엔티티는 soft delete보다 hard delete가 성능/관리 면에서 유리한 경우가 많음
5. JPA스럽게 사고하기
- JPA는 엔티티 중심의 객체지향적인 설계를 지향한다.
- 상태 변경은 엔티티 내부 메서드에서 수행되도록 하고, repository는 저장/조회 역할에만 집중시킨다.
- 연관관계 설정이나 연산은 서비스에서 직접 하지 않고, 의미 있는 메서드를 통해 위임하는 것이 유지보수에 유리하다.
6. 테스트 전략 및 유의사항
- 단위 테스트는 핵심 로직의 의도를 기준으로 검증하며, 부수 결과까지 모두 assert할 필요는 없음
- DB 상태를 확인하려면 @Commit을 붙여 트랜잭션을 커밋하거나 flush() 호출이 필요함
- 테스트 환경의 DB URL과 콘솔에서 조회하는 DB URL이 같은지 반드시 확인해야 함 (H2는 tcp/embedded에 따라 분리될 수 있음)
- 테스트를 반복 실행하면 중복 저장으로 충돌 가능성이 있으므로, 고유 값 또는 테스트 데이터 삭제 전략이 필요함