이번 Spring 섹션에서는 스프링부트를 사용하면서 마주쳤던 트러블 슈팅 및 개인 공부에 대해서 정리하려고 합니다.
* 인프런의 강의를 들으며 생겼던 개인적인 궁금증, 개인 프로젝트를 진행하면서 마주치는 이슈들입니다.
오늘의 요약 : @Transactional을 활용한 엔티티 수정 방식 비교하고 예외 처리 유무에 대한 정리
Spring Data JPA로 프로젝트를 진행하다 보면 엔티티를 수정하는 코드를 자주 작성하게 됩니다. 비슷해 보이지만 세부적인 구현 방식에 따라 가독성과 안정성이 달라질 수 있습니다. 두 가지 코드 스타일을 비교하며 어떤 차이가 있고, 어떤 방식이 더 권장되는지 정리해 보겠습니다.
1. 두 코드 비교하기
먼저 비교할 두 코드는 다음과 같습니다.
공통적으로 아이디, 닉네임을 찾아서 업데이트 하는 코드입니다.
@Transactional
public User update(Long id, String nickname, String bio) {
User user = findById(id);
user.update(nickname, bio);
return user;
}
@Transactional
public User updateUser(Long id, String nickname, String bio) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserException("사용자를 찾을 수 없습니다: " + id));
user.updateNickname(nickname, bio);
return user;
}
2. 주요 차이점 2가지
① 명확한 예외 처리 (Optional 활용)
스타일 B는 다음과 같이 Optional과 orElseThrow()를 사용합니다.
이 방식의 장점은 데이터가 존재하지 않을 경우의 흐름을 서비스 레이어에서 명확하게 정의할 수 있다는 점입니다.
단순히 null을 반환이 아니라
- 어떤 상황에서
- 어떤 예외를
- 어떤 메시지로 던질지
를 명시적으로 제어할 수 있는 장점이 있습니다.
반면 스타일 A에서 사용하는 findById(id)가 내부적으로 어떤 예외를 던지는지는 메서드 구현을 보지 않으면 알기 어렵습니다.
즉, 예외 처리의 의도가 코드에 드러나지 않는다는 단점이 있습니다.
서비스 레이어에서는 이런 “없을 수 있음”을 명확히 드러내는 코드가 가독성과 유지보수 측면에서 유리합니다.
② 수정 로직의 캡슐화 (도메인 중심 설계)
스타일 A를 살펴보겠습니다.
이는 엔티티 내부에 수정 로직을 캡슐화했습니다.
단순히 setNickname(), setBio()를 나열하는 대신,
- “닉네임과 소개글을 변경한다”
- 즉, 프로필을 수정한다
라는 비즈니스 의미를 메서드 이름에 담고 있습니다.
이렇게 하면 다음과 같은 장점이 있다.
- 필드 변경 규칙(검증, 길이 제한 등)을 한 곳에서 관리할 수 있고
- 수정 정책이 바뀌어도 서비스 코드는 수정할 필요가 없으며
- 엔티티가 단순한 데이터 덩어리가 아닌 도메인 객체로 역할을 하게 된다
도메인 로직은 서비스가 아니라 엔티티가 책임지는 구조가 더 유지보수에 유리하다.
정리하면
- 스타일 B는
→ 예외 흐름을 명확히 제어한다는 점에서 서비스 레이어 관점에서 안정적 - 스타일 A는
→ 도메인 로직을 엔티티에 캡슐화했다는 점에서 객체지향적으로 우수
cf) 3. 공통 : Dirty Checking (변경 감지)
두 코드의 공통점이자 가장 중요한 포인트는 userRepository.save()를 호출하지 않는다는 점입니다.
이것이 가능한 이유는 @Transactional 덕분입니다.
- 트랜잭션 안에서 엔티티를 조회하면, JPA는 해당 엔티티의 초기 상태를 스냅샷으로 찍어둡니다.
- 트랜잭션이 끝나는 시점에 현재 엔티티의 상태와 스냅샷을 비교합니다.
- 변경사항이 있다면 JPA가 자동으로 UPDATE 쿼리를 생성해서 DB에 날립니다.
cf) setter를 지양하는 이유
엔티티에 setter를 무분별하게 열어두는 것은 초기에는 편해 보이지만,
프로젝트가 커질수록 여러 문제를 만듭니다.
① 변경 의도가 드러나지 않는다
이 코드를 봤을 때 우리는 알 수 있는 게 거의 없습니다.
- 단순 수정인지
- 프로필 수정인지
- 특정 정책이 적용되어야 하는 변경인지
모든 변경이 동일한 형태의 setter 호출로 표현되기 때문입니다.
반면 다음 코드를 보면
이 한 줄만 봐도
“아, 이 유저의 프로필을 수정하는구나”라는 비즈니스 의도가 명확히 드러납니다.
② 검증 로직이 흩어진다
setter를 사용하면 보통 이런 코드가 서비스 레이어에 생깁니다.
문제는 같은 검증 로직이
- 회원 정보 수정
- 관리자 수정
- 배치 작업
등 여러 곳에 중복되기 쉽다는 점입니다.
또한 setter 위주의 설계는 엔티티를 행동 없는 DTO로 만들어 버립니다.
마치며
단순히 기능이 동작하게 만드는 것을 넘어, "데이터가 없을 때 어떻게 예외를 처리할 것인가?"와 "수정 로직을 얼마나 의미 있게 묶을 것인가?"를 고민해야 한다는 것입니다.
'Spring' 카테고리의 다른 글
| [Spring] Spring WebFlux와 Spring MVC - 1 (0) | 2026.02.11 |
|---|---|
| [Spring] controller에서 repository를 참조하는 코드를 짜면? (0) | 2026.02.02 |
| [Spring] 필터와 인터셉터 (0) | 2026.01.31 |
| [Spring] 컨테이너가 빈을 생성하는 과정 (0) | 2026.01.29 |
| [Spring] IoC, DI (0) | 2026.01.28 |