Service 작성 가이드

Service 레이어 작성 패턴, 트랜잭션, 비즈니스 로직 구성 규칙

마지막 수정: 2026-05

Service 작성 가이드

기본 원칙

  • Interface 작성 안 함 — Service 클래스 직접 사용
  • @Service, @RequiredArgsConstructor, @Slf4j 필수
  • 쓰기 트랜잭션: @Transactional(rollbackFor = Exception.class)
  • 읽기 트랜잭션: @Transactional(readOnly = true)
  • HTTP 관련 객체 (HttpServletRequest, HttpSession) 직접 사용 금지
  • 하나의 메서드는 하나의 역할만 수행

서비스 클래스 기본 구조

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final UserMapper userMapper;

    /**
     * 사용자 단건 조회
     */
    @Transactional(readOnly = true)
    public UserVo get(UserVo vo) {
        return userMapper.get(vo);
    }

    /**
     * 사용자 목록 조회
     */
    @Transactional(readOnly = true)
    public List<UserVo> getList(UserVo vo) {
        return userMapper.getList(vo);
    }

    /**
     * 사용자 전체 수 조회
     */
    @Transactional(readOnly = true)
    public int getTotalCount(UserVo vo) {
        return userMapper.getTotalCount(vo);
    }

    /**
     * 사용자 등록
     */
    @Transactional(rollbackFor = Exception.class)
    public void regist(UserVo vo) {
        userMapper.regist(vo);
    }

    /**
     * 사용자 수정
     */
    @Transactional(rollbackFor = Exception.class)
    public void update(UserVo vo) {
        userMapper.update(vo);
    }

    /**
     * 사용자 삭제
     */
    @Transactional(rollbackFor = Exception.class)
    public void delete(UserVo vo) {
        userMapper.delete(vo);
    }
}

트랜잭션 규칙

메서드 성격트랜잭션 어노테이션
조회 (SELECT)@Transactional(readOnly = true)
쓰기 (INSERT/UPDATE/DELETE)@Transactional(rollbackFor = Exception.class)
// 읽기 전용 트랜잭션
@Transactional(readOnly = true)
public UserVo get(UserVo vo) {
    return userMapper.get(vo);
}

// 쓰기 트랜잭션 — 모든 예외에서 롤백
@Transactional(rollbackFor = Exception.class)
public void regist(UserVo vo) {
    userMapper.regist(vo);
}

복합 트랜잭션 (여러 Mapper 호출)

@Transactional(rollbackFor = Exception.class)
public void registOrder(OrderVo vo) {
    // 주문 등록
    orderMapper.regist(vo);

    // 주문 상세 등록 (같은 트랜잭션)
    for (OrderDetailVo detail : vo.getDetailList()) {
        detail.setOrderSn(vo.getOrderSn());
        orderDetailMapper.regist(detail);
    }

    // 재고 차감
    stockMapper.decrease(vo);
}

비즈니스 검증

Service에서 비즈니스 규칙을 검증한다.

@Transactional(rollbackFor = Exception.class)
public void regist(UserVo vo) {
    // 비즈니스 규칙 검증
    if (userMapper.existsByUserId(vo.getUserId()) > 0) {
        throw new BusinessException("이미 사용 중인 아이디입니다.");
    }

    userMapper.regist(vo);
    log.info("사용자 등록 완료: userId={}", vo.getUserId());
}

로깅

// 주요 비즈니스 이벤트 INFO 로그
log.info("사용자 등록: userId={}", vo.getUserId());

// 경고 상황 WARN 로그
log.warn("사용자 조회 결과 없음: userSn={}", vo.getUserSn());

// 예외 ERROR 로그
log.error("사용자 등록 실패: userId={}", vo.getUserId(), e);


Lombok — @SuperBuilder 주의사항

@SuperBuilder + 스트림 타입 추론 주의

@SuperBuilder가 적용된 클래스를 빌더로 생성할 때 스트림 .toList()에서 타입 추론 실패가 발생할 수 있다.

// ❌ 컴파일 에러 — List<CAP#1> cannot be converted to List<ChildDto>
List<ChildDto> result = items.stream()
        .map(i -> ChildDto.builder().field(i.getField()).build())
        .toList();

// ✅ for-loop으로 해결
List<ChildDto> result = new ArrayList<>();
for (Item i : items) {
    result.add(ChildDto.builder().field(i.getField()).build());
}

체크리스트

  • [ ] Interface 작성 안 함
  • [ ] @Service, @RequiredArgsConstructor, @Slf4j 선언
  • [ ] 읽기 메서드: @Transactional(readOnly = true)
  • [ ] 쓰기 메서드: @Transactional(rollbackFor = Exception.class)
  • [ ] HTTP 관련 객체 미사용
  • [ ] 비즈니스 검증은 Service 레이어에서 처리