관리 메뉴

Life goes slowly...

[JPA]JPA 로 개발하면서 자주 하는 실수 Top 5 본문

프로그래밍/Java

[JPA]JPA 로 개발하면서 자주 하는 실수 Top 5

빨강소 2026. 1. 27. 09:41
728x90
반응형

 

<출처:grok AI>

JPA 초보자가 자주 하는 실수 Top 5 

JPA(Java Persistence API)는 자바 개발자에게 ORM(Object-Relational Mapping)의 강력함을 제공하여 객체 지향적으로 데이터베이스와 상호작용할 수 있게 해줍니다. 하지만 강력한 만큼 제대로 사용하지 않으면 예상치 못한 문제에 부딪힐 수 있습니다. JPA를 처음 접하는 개발자들이 자주 하는 실수 5가지와 이를 피하는 방법을 알아보겠습니다.

 


1. Entity에 비즈니스 로직 넣기 → Service로 빼기

문제점: Entity는 데이터베이스 테이블과 매핑되는 순수한 객체여야 합니다. 여기에 비즈니스 로직이 들어가면 Entity의 역할이 모호해지고, 재사용성이 떨어지며, 테스트하기 어려워집니다. Entity는 데이터의 상태를 표현하는 데 집중해야 합니다.

 

해결 방안: 비즈니스 로직은 Service 계층으로 분리해야 합니다. Service 계층은 여러 Entity나 Repository를 조합하여 복잡한 비즈니스 규칙을 처리하는 역할을 합니다.

 

예시:

// bad: Entity에 비즈니스 로직 포함
@Entity
public class Order {
    private int quantity;
    private int price;

    public void calculateTotalPrice() {
        // 비즈니스 로직이 Entity 내부에 존재
        return quantity * price;
    }
}

// good: Service 계층에서 비즈니스 로직 처리
@Service
public class OrderService {
    public int calculateTotalPrice(Order order) {
        return order.getQuantity() * order.getPrice();
    }
}

 

 

2. Controller에서 Repository 직접 호출 → Service 거치기

문제점: Controller는 웹 요청을 받고 응답을 반환하는 역할을 합니다. Controller에서 직접 Repository를 호출하면 비즈니스 로직이 Controller에 노출되거나, Controller가 너무 많은 책임을 지게 됩니다. 이는 계층 간의 의존성을 높이고 코드의 응집도를 떨어뜨립니다.

 

해결 방안: Controller는 Service 계층을 통해 비즈니스 로직을 위임해야 합니다. Service 계층은 트랜잭션 관리와 비즈니스 로직 처리를 담당하여 Controller의 역할을 가볍게 만듭니다.

 

예시:

// bad: Controller에서 Repository 직접 호출
@RestController
public class OrderController {
    @Autowired
    private OrderRepository orderRepository;

    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        return orderRepository.findById(id).orElse(null);
    }
}

// good: Controller에서 Service 호출
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping("/orders/{id}")
    public OrderDto getOrder(@PathVariable Long id) {
        return orderService.findOrderById(id);
    }
}

 

 

3. @Transactional 안 쓰기 → 데이터 깨짐 위험

문제점: 데이터베이스의 무결성을 유지하기 위해서는 여러 작업이 하나의 논리적인 단위로 묶여 처리되어야 합니다. @Transactional 어노테이션을 사용하지 않으면 각 데이터베이스 작업이 독립적으로 실행되어 데이터 일관성이 깨지거나, 예상치 못한 오류 발생 시 롤백이 되지 않아 데이터가 손상될 수 있습니다.

 

해결 방안: 데이터 변경이 일어나는 모든 Service 메서드에는 @Transactional을 적용해야 합니다. 이를 통해 메서드 내의 모든 데이터베이스 작업이 하나의 트랜잭션으로 묶여 원자성(Atomicity)을 보장받을 수 있습니다.

 

예시:

// bad: @Transactional 없이 데이터 변경
@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    public void updateProductPrice(Long productId, int newPrice) {
        Product product = productRepository.findById(productId).orElseThrow();
        product.setPrice(newPrice);
        // save를 호출해도 트랜잭션 범위가 아니므로 예외 발생 시 롤백 안됨
    }
}

// good: @Transactional 적용
@Service
@Transactional
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    public void updateProductPrice(Long productId, int newPrice) {
        Product product = productRepository.findById(productId).orElseThrow();
        product.setPrice(newPrice);
        // 트랜잭션 내에서 처리되므로 안정성 보장
    }
}

 

 

4. Entity를 그대로 Response로 주기 → DTO로 변환 필수

문제점: Entity는 애플리케이션의 핵심 비즈니스 로직과 밀접하게 연관되어 있으며, 데이터베이스의 구조를 반영합니다. Entity를 HTTP 응답으로 직접 반환하면 다음과 같은 문제가 발생할 수 있습니다.

  • 보안 문제: 민감한 정보(예: 비밀번호, 내부 ID)가 외부에 노출될 수 있습니다.
  • 유연성 부족: Entity의 필드가 변경될 때마다 API 스펙도 변경되어야 합니다.
  • 불필요한 데이터 전송: 클라이언트에게 필요 없는 필드까지 함께 전송되어 네트워크 오버헤드가 발생합니다.
  • 무한 재귀 참조: 양방향 연관관계가 있는 Entity의 경우 JSON 직렬화 시 무한 루프가 발생할 수 있습니다.

해결 방안: 클라이언트에게 데이터를 반환할 때는 항상 DTO(Data Transfer Object)로 변환하여 사용해야 합니다. DTO는 클라이언트에게 필요한 데이터만 포함하며, API 스펙을 Entity와 분리하여 관리할 수 있게 해줍니다.

 

예시:

// bad: Entity를 직접 반환
@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userRepository.findById(id).orElse(null); // User Entity 반환
    }
}

// good: DTO로 변환하여 반환
public class UserDto {
    private String name;
    private String email;
    // ... 필요한 필드만 포함
}

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return userService.findUserDtoById(id); // UserDto 반환
    }
}

 

 

5. 모든 메서드에 @Transactionsal(readOnly = true) 를 쓰지 않는것

문제점: 조회 전용 메서드임에도 불구하고 불필요한 스냅샷 생성하여 Dirty Checking 비용이 발생한다

 

해결 방안: 조회용 메서드는 반드시 readOnly를 붙이는 습관을 갖고 개발을 진행하는것이 좋다. 이로써 성능의 차이가 20~50% 이상 나는 경우도 흔하다.

 

예시:

// 읽기 메서드인데 readOnly = true 없음 → 불필요한 dirty checking, 2nd level cache 활용 못 함
public List<Order> findRecentOrders() { ... }

// 반대로 모든 메서드에 readOnly = true 붙여놓고 중요한 쓰기 메서드도 그대로 → 데이터 안 저장됨

//추천하는 설정방법
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderQueryService {

    // 대부분의 메서드는 readOnly 그대로 사용

    @Transactional
    public void someModifyMethod() { ... }   // 필요한 곳에만 override
}

 

728x90
반응형
Comments