[Java&Spring 면접 준비] Day 3 - Spring 기초 및 JPA

seri·2025년 8월 5일
0

1. Spring에서 DTO와 Entity를 분리해서 사용하는 이유는 무엇인가요?

실무에서 REST API를 개발하면서 항상 DTO와 Entity를 분리해서 사용했습니다. 그 이유는 먼저 Entity는 데이터베이스와 직접 매핑되기 때문에 불필요한 정보가 외부로 노출될 수 있고, 변경 시 의도치 않게 DB 구조까지 영향을 줄 수 있습니다.

반면 DTO는 요청/응답에 맞게 필요한 필드만 포함시키고, 보안적으로도 민감 정보는 제외할 수 있어서 안정적입니다.
예를 들어, 유저 정보를 조회할 때 비밀번호나 권한은 숨기고 이름, 이메일만 반환하는 UserResponseDto를 따로 만들어 사용했었습니다.

꼬리질문:

→ 그럼 DTO를 Entity로 변환할 땐 어떤 방식 쓰셨어요?

네, 작은 프로젝트에서는 수동 매핑을 선호했어요. 명시적으로 어떤 필드가 매핑되는지 알 수 있어서요. 대규모 프로젝트에서는 MapStruct도 일부 도입해서 자동화한 적 있습니다.


2. Spring Data JPA에서 Pageable을 어떻게 사용해보셨나요?

게시글 목록이나 사용자 활동 이력 같은 리스트 조회 기능에서 자주 사용했습니다.

컨트롤러에서는 @RequestParam으로 page, size, sort 정보를 받고, 서비스에서 PageRequest.of()Pageable 객체를 생성해서 Repository에 넘겼습니다.

예를 들어 Page<Post> 형태로 결과를 받아서 getContent(), getTotalPages() 등을 추출해 API 응답 포맷에 맞게 DTO로 가공했습니다.

3. @Transactional 어노테이션의 역할은 무엇인가요?

@Transactional은 트랜잭션 범위를 지정하는 어노테이션으로, 메서드 실행 중 예외가 발생하면 자동으로 롤백시켜줍니다.

실무에선 서비스 계층에 주로 붙여서 사용했고, 특히 여러 Repository가 동시에 변경되는 작업에서 유용했습니다.

꼬리질문:

readOnly = true 옵션은 써보셨나요?

네, 조회 전용 메서드에는 @Transactional(readOnly = true)를 붙이면 성능이 조금 향상되는 걸로 알고 있고, JPA가 더 이상 dirty checking을 하지 않아서 불필요한 리소스를 아낄 수 있었습니다.

→ 트랜잭션을 왜 서비스 계층에 걸었나요? 컨트롤러나 Repository에 걸면 안 되나요?

네, 트랜잭션은 서비스 계층에 걸어야 한다고 생각합니다.
왜냐하면 서비스 계층이 실제 비즈니스 로직을 처리하고 여러 DAO나 외부 호출을 조합하는 곳이기 때문입니다.
컨트롤러는 단순히 요청을 전달하는 역할이고, Repository는 DB 단위의 세부작업만 담당하니까, 트랜잭션 책임을 맡기기엔 적절하지 않다고 봅니다.
또한 서비스 계층에 트랜잭션을 설정하면 하위의 여러 Repository 호출이 하나의 트랜잭션으로 묶여서 처리되기 때문에, 정합성 유지에도 유리합니다.

→ 전체 주문을 하나의 트랜잭션으로 묶었는데, 개별 주문마다 트랜잭션을 나누는 방식은 고려 안 해보셨나요?

네, 사실 두 가지 방식을 비교해봤습니다.
하지만 당시 업무 요구사항에서는 한 번의 수집 배치에서 모든 주문이 가공·저장돼야 '완전한 단위'로 간주됐습니다.
그래서 일부 주문만 저장되고, 나머지가 빠지는 상황은 피해야 했습니다.
예를 들어 수집한 50건 중 48건만 저장되면, 다시 수집하거나 보정하는 작업이 더 복잡해집니다.
다만 예외 처리나 재시도 정책이 있다면 개별 트랜잭션도 고려해볼 수 있을 것 같습니다.

→ 이 작업은 배치로 동작했는데, 트랜잭션이 너무 커지면 성능 문제는 없었나요?

맞습니다. 그래서 한 번에 수백 건씩 처리하지 않고, 청크 단위로 잘라서 트랜잭션을 관리했습니다.
1000건 단위로 나눠서 수집하고, 청크별로 실패 시 별도 재처리 큐로 넘기는 방식으로 구현했어요.
또한 배치 특성상 사용자와 실시간 인터랙션이 없기 때문에, 어느 정도 트랜잭션 시간이 길어져도 문제는 없었고, DB 연결 시간이나 커넥션 풀 제한만 조심해서 튜닝했습니다.

→ 트랜잭션 적용 시 성능 이슈는 없었나요?

초기에는 트랜잭션이 너무 길어져서 락이 오래 잡히는 문제가 있었습니다.
그래서 가공이 오래 걸리는 부분은 트랜잭션 외부로 분리했고, DB 저장이 필요한 로직만 트랜잭션 내부에 최소화해서 넣는 식으로 개선했습니다.
또한 가능하면 readOnly = true를 명시하거나, 조회 로직은 분리해서 처리하면서 성능 이슈를 최소화했습니다.

→ Propagation.REQUIRED vs REQUIRES_NEW

REQUIRED는 기존 트랜잭션이 있으면 그 안에 합류하는 방식이고, REQUIRES_NEW는 기존 트랜잭션을 잠시 보류하고 새로운 트랜잭션을 시작합니다.
그래서 주문 처리 중 실패하더라도 로그는 반드시 DB에 남겨야 하는 경우라면, REQUIRES_NEW를 사용해야 합니다.
실무에서는 로그, 이력, 알림 전송 같은 비핵심 부가 로직을 REQUIRES_NEW로 분리해놓고 트러블슈팅에 활용하는 경우가 많았습니다.


4. Controller, Service, Repository의 역할을 구분해서 설명해주실 수 있나요?

네, 실무에서도 이 구조를 명확히 구분해서 작업했습니다.

  • Controller는 요청을 받아서 DTO로 파싱하고, 응답을 만드는 역할만 담당했습니다.
  • Service는 실제 비즈니스 로직이 구현되는 곳으로, 트랜잭션 관리도 여기서 담당했습니다.
  • Repository는 JPA를 사용해 DB와 직접 통신하는 역할을 했고, 주로 CRUD 또는 JPQL 쿼리를 정의했습니다.

예를 들어 주문 수집 기능을 만들 때, Controller에서는 주문 수집 요청을 받고, Service에서는 API에서 받아온 주문 데이터를 가공해 처리했고, Repository에서는 최종 주문 수집 데이터를 저장했습니다.

꼬리질문:

→ Controller에서 바로 Repository 호출하면 안 되나요?

단기적인 개발에서는 가능할 수도 있지만, 그렇게 되면 재사용성이나 유지보수성이 떨어집니다.
특히 서비스 로직이 복잡해질 경우 컨트롤러가 비대해지고, 트랜잭션 처리도 어려워지기 때문에 분리를 유지하는 게 좋다고 생각합니다.

→ @Valid 어노테이션을 사용해보신 적 있나요?

네, 사용해봤습니다.
주로 컨트롤러에서 클라이언트로부터 전달받은 DTO 객체의 유효성 검사를 할 때 사용했습니다.
예를 들어, 사용자 생성 API에서 @RequestBody로 받은 DTO에 @NotBlank, @Size 같은 제약을 지정해두고,
컨트롤러 메서드 파라미터에 @Valid를 붙여서 요청값 검증이 자동으로 수행되도록 했습니다.


5. JPA에서 양방향 연관관계를 설정할 때 주의할 점은?

가장 중요한 건 연관관계의 주인을 명확히 설정하는 거라고 생각합니다. 주인은 외래키를 관리하는 쪽이고, 보통 @ManyToOne 쪽이 주인이 되죠.
그리고 양쪽 필드 값을 동시에 관리해줘야 데이터 정합성이 깨지지 않기 때문에, addXXX() 같은 편의 메서드를 만들어서 두 객체에 모두 값을 넣어주는 식으로 처리했습니다.

실무에서는 사실 양방향 연관관계를 자주 사용하지 않았습니다.
왜냐하면 연관관계를 잘못 설정하면 무한 루프, 데이터 정합성 문제, 예상치 못한 N+1 문제 등 다양한 이슈가 발생하기 쉽습니다.
대신 단방향 연관관계를 기본으로 두고, 필요한 경우에만 명확한 주체를 정해서 양방향으로 설정했습니다.
그리고 연관된 데이터를 조회할 필요가 있을 땐 JPQL, QueryDSL, DTO projection을 활용하는 식으로 설계를 했습니다.
양방향보다는 단방향이 설계가 단순하고 유지보수도 쉬워서, 가능하면 단방향으로 구성하는 걸 선호했습니다.

꼬리질문:

→ Cascade 옵션도 사용해보셨나요?

네, 부모-자식 관계에서 자식 엔티티를 함께 저장하거나 삭제할 때 CascadeType.ALL 또는 REMOVE를 사용했습니다.
단, 무분별하게 설정하면 예기치 않은 삭제가 발생할 수 있어서 꼭 필요한 경우에만 적용했습니다.

→ JPQL vs QueryDSL

구분 JPQL QueryDSL
작성 방식 문자열 기반 Java 코드 기반 (타입 안전)
적합한 상황 단순한 쿼리, 정적 쿼리, 복잡하지 않은 join 복잡한 조건, 동적 쿼리, 조건이 많은 검색
가독성 짧은 쿼리는 오히려 보기 쉬움 쿼리가 길어지면 유지보수 편함
재사용성 재사용 어려움 (하드코딩된 문자열) 메서드로 분리해 재사용 용이
런타임 오류 존재 가능 (문자열 파싱 실패 등) 컴파일 타임 오류로 사전 방지 가능
DTO 매핑 new 키워드 사용, 오타 위험 Projections.bean 등으로 안정적 매핑 가능
단점 유지보수 어려움, IDE 지원 약함 초기 세팅 번거롭고 문법 장벽 있음

단순한 정적 쿼리의 경우에는 JPQL을 사용했습니다.
예를 들어, 특정 상태값을 가진 리스트를 조회하거나 단순한 정렬이 필요한 경우 등입니다.

반면에, 조건이 유동적으로 바뀌는 검색 화면이나 필터 조건이 많은 쿼리의 경우에는 QueryDSL을 사용했습니다.
예를 들어, 검색 조건이 5~6개 이상이고 where 절이 동적으로 조합되어야 하는 경우,
BooleanBuilder 또는 where(...).and(...) 구조를 쓰는 QueryDSL이 훨씬 유지보수에 유리했습니다.

profile
꾸준히 정진하며 나아가기

0개의 댓글