GPT 에게 질문한 내용 ? JPA를 개발할때 사고(생각) 흐름이 궁금해.
🔁 실제 사고 흐름 정리 (예시)
예: "사용자가 상품을 주문한다" 라는 기능을 개발한다고 하자
- 사용자는 어떤 행동을 하는가? → 주문을 생성
- 이 과정에서 어떤 도메인이 필요한가? → User, Product, Order
- 각 도메인은 어떤 필드를 가져야 하는가? 관계는 어떤가?
- 주문 저장 시 어떤 로직이 필요할까? (트랜잭션 여부 포함)
- 프론트에서 어떤 데이터를 보내고 받아야 하지?
- 그걸 처리할 API는 어떤 구조여야 하지?
💡 실무 팁
- 우선순위는 "기능 단위"로 묶어서 생각해라
- ex) "주문 생성" 기능 → 이 기능에 필요한 최소 도메인, 쿼리, API 부터 설계
- 기능 단위로 작은 범위를 반복적으로 개발해라 (TDD 또는 기능 단위 커밋)
- 회사 코드 스타일 따라가기가 중요함 (QueryDSL, 패키지 구조, 네이밍 등)
위 내용을 바탕으로 실제 JPA 개발시 적용해 생각해보며 개발한 내용을 정리해보고자 합니다.
✅ 1. 요구사항 분석 → 도메인 설계
✅ 2. 엔티티(Entity) 설계 : 📌 사고: 어떤 테이블이 필요한가?
✅ 3. Repository 작성
✅ 4. Service 설계
✅ 5. Controller / API 설계
✅ 6. 예외 설계
✅ 7. 테스트 코드 설계(단위,통합 테스트 기준의 사고)
✅ 1. 요구사항 분석
요구사항으로 간단한 예시를 들려고 합니다. 일정을 등록하는 화면이 있다고 가정하고, 회원이 해당 모임(크루)의 일정을 등록하는
요구사항을 GPT에게 물어본 사고 방식대로 개발을 진행하려고 합니다.
*생각한 요구사항
- 사용자가 일정을 추가하는 로직
- 일정의 유형은 개인/정기 로 나뉜다.
- 입력값은 제목, 날짜(시간) , 장소, 공지사항(설명) 을 입력할수 있다.
- 등록자는 자동으로 일정의 책임자가 된다.
- 등록과 동시에 등록자는 참석자가 된다.
✅ 2. 도메인 작업
요구사항에 대해서 어떤 테이블이 필요한지 생각해보겠습니다.
- User : 사용자
- Event : 일정
- EventAttendee : 일정 참석자
가 필요하다고 생각하였고, 각각의 Entity를 구성하였습니다. 아래는 Event 엔터티 코드이며, User와의 관계인 @ManyToOne에 대해서 생각해보겠습니다.
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(nullable = false, updatable = false, columnDefinition = "VARCHAR(36)")
private String id;
private String title;
// private String location;
@Embedded // id 값이 없는 객체 포함
private Location location;
private String description;
private LocalDateTime eventDateTime;
@Enumerated(EnumType.STRING)
private EventType eventType;
@ManyToOne(fetch = FetchType.LAZY) // 소유자는 여러 이벤트에 있을수 있다. 이벤트는 소유자에 속해있다(반대로 말하면 사용자는 여러 이벤트를 만들수 있다)
private User owner;
}
@ManyToOne : 의 경우 이 엔터티는 이 컬럼에 속해있다 라고 해석하면 됩니다. 즉 일정(Event)는 하나의 사용자(User) 책임자로
속해 집니다.
User 엔터티는 설명을 생략하고, 일정 참석자인 EventAttendee 에 대해서 이야기 해보겠습니다.
package com.crewManager.pro.event.domain;
import com.crewManager.pro.crew.domain.Crew;
import com.crewManager.pro.user.domain.User;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class EventAttendee {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@ManyToOne(fetch = FetchType.LAZY) // 이벤트는 여러 참석자가 가능하다. / 참석자는 이벤트에 속해 있다.
private Event event;
@ManyToOne(fetch = FetchType.LAZY) // 사용자는 여러 참석자가 될수 있다. / 참석자는 사용자에 속해있다.
private User user;
}
마찬가지로 참석자(EventAttendee)는 이벤트에 속해있고 이벤트는 여러 참석자가 가능합니다. 또한
참석자(EventAttendee)는 사용자(User)에 속해있고, 여러 참석자로 소속이 가능합니다.
이렇게 기본적은 MVP 엔터티 구성이 끝났으며, 레파지토리를 구성해 봅니다.
✅ 3. Repository 작성
기본 JpaRepository를 상속받는 인터페이스를 만들어 놓고, 사고에 따라 추가합니다.
(existByEventAndUser 라는 메소드 쿼리는 추후 나아가며 예외처리 과정에서 작성되었습니다.)
public interface EventAttendeeRepository extends JpaRepository<EventAttendee,String> {
// 간단한 코드는 Query 메소드로 구현.
boolean existByEventAndUser(Event event, User user);
}
public interface EventRepository extends JpaRepository<Event,String> {
}
✅ 4. Service 설계
이제 부터 실제 서비스에 대해서 사고를 해봅니다. User가 일정(Event)를 등록하여 참여하기 위해서는
어떤 로직이 필요할까요 ?
먼저 현재 User에 대해서 조회를 해야합니다. 조회후에 Event를 등록합니다. 등록후 참석자로 등록을 합니다.
서비스 코드를 살펴보겠습니다. userSerivce에서 예전에 이미 이메일 기반으로 User를 조회하는 것이 있어 활용하여
유저를 우선 조회 하였습니다.
(코드에 나와 있는 예외의 경우 6번 단계인 예외 단계에서 살펴보겠습니다.)
조회 후에 Event를 화면에서 받은 req 값 , 조회한 User 값을 활용하여 save 합니다.
이과정에서 생각 한 내용
- req를 위한 DTO를 구성해야겠다 -> createEventRequest 구현
- Event를 set하기 위해서 builder를 적용.
- 필드값인 location의 경우 추후 카카오맵이나 다른 것들을 활용 목적으로 @Embeddable 객체 구성
Event가 save 되고 나서 참석자에 대한 엔터티도 save 합니다. 화면에서 등록이 완료됐을때 활용할만한 내용으로
사용자 이름을 return 하였습니다.
@Transactional
public String createEvent(CreateEventRequest createEventRequest){
//1.현재 user 조회
User user = userService.findUserByEmail(createEventRequest.getUserEmail());
// 날짜는 존재해야함.
if(createEventRequest.getEventDateTime() == null || createEventRequest.getEventDateTime().isBefore(LocalDateTime.now())){
throw new BusinessException(ErrorCode.INVALID_EVENT_DATE);
}
// 타이틀과 위치는 존재해야함.
if(createEventRequest.getTitle() == null || createEventRequest.getLocation() == null ||
createEventRequest.getTitle().isBlank() || createEventRequest.getLocation() == null){
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
//2. 이벤트 저장.
Event event = Event.builder()
.title(createEventRequest.getTitle())
.description(createEventRequest.getDescription())
.owner(user)
.eventDateTime(createEventRequest.getEventDateTime())
.location(createEventRequest.getLocation())
.eventType(createEventRequest.getEventType())
.build();
Event saved = eventRepository.save(event);
boolean alreadyExists = eventAttendeeRepository.existByEventAndUser(saved,user);
// 이미 존재하는지 여부 판단.
if(alreadyExists){
throw new BusinessException(ErrorCode.CREW_USER_DUPLICATED);
}
EventAttendee eventAttendee = EventAttendee.builder()
.event(event)
.user(user)
.build();
eventAttendeeRepository.save(eventAttendee);
return saved.getOwner().getName();
}
@Embeddable 객체란 실제로 db로는 사용하지 않지만 엔터티에 활용할 id 값이 없는 객체로 사용됩니다.
@Embeddable
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Location {
private String locationName;
private String address;
private Double latitude;
private Double longitude;
}
✅ 5. Controller / API 설계
만든 서비스를 controller에 연결하여 api로 활용합니다.
@RequestMapping("/create")
public ResponseEntity<String> createEvent(@RequestBody @Valid CreateEventRequest req){
String name = eventService.createEvent(req);
return ResponseEntity.ok(name);
}
✅ 6. 예외 설계
실제 서비스에서 어떤 예외가 있을지 생각해 봅시다.
사용자가 일정을 등록할때 어떤 예외가 있을까요 ?
1. 화면에서 처리를 어느정도 하겠지만, 일정에 있어 날짜는 필수이기 때문에 날짜에 대한 예외처리를 진행.
2. 위치도 날짜와 동일하게 확인 . 여기서는 제목도 예외처리하였습니다.
3. 유저와 이벤트에 대해서 이미 참석한 사람인지도 체크 할 필요가 있었습니다.
기존에 Enum으로 관리하고 있는 BusinessException을 구현해 놓은게 있어 이를 활용하였습니다.
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() { return errorCode; }
}
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다."),
CREW_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 크루를 찾을 수 없습니다."),
CREW_USER_DUPLICATED(HttpStatus.CONFLICT, "이미 존재하는 회원 입니다."),
CREW_NAME_DUPLICATED(HttpStatus.CONFLICT, "이미 존재하는 크루 이름입니다."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "입력값이 올바르지 않습니다."),
INVALID_EVENT_DATE(HttpStatus.BAD_REQUEST, "날짜가 올바르지 않습니다.");
private final HttpStatus status;
private final String message;
}
이 과정에서 EventAttendeeRepository에서 메소드 쿼리를 이용하여 existByEventAndUser를 구현하여 적용하였습니다.
- 실무에서는 일반적으로 간단한 쿼리의 경우엔 메소드 쿼리를 이용합니다. 이경우에는 단순히 boolean을 판단하는 간단한 로직이기 때문에 메소드 쿼리로 진행하였습니다.
public interface EventAttendeeRepository extends JpaRepository<EventAttendee,String> {
// 간단한 코드는 Query 메소드로 구현.
boolean existByEventAndUser(Event event, User user);
}
실제로 예외 까지 완성된 서비스 코드는 아래와 같습니다.
@Transactional
public String createEvent(CreateEventRequest createEventRequest){
//1.현재 user 조회
User user = userService.findUserByEmail(createEventRequest.getUserEmail());
// 날짜는 존재해야함.
if(createEventRequest.getEventDateTime() == null || createEventRequest.getEventDateTime().isBefore(LocalDateTime.now())){
throw new BusinessException(ErrorCode.INVALID_EVENT_DATE);
}
// 타이틀과 위치는 존재해야함.
if(createEventRequest.getTitle() == null || createEventRequest.getLocation() == null ||
createEventRequest.getTitle().isBlank() || createEventRequest.getLocation() == null){
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
}
//2. 이벤트 저장.
Event event = Event.builder()
.title(createEventRequest.getTitle())
.description(createEventRequest.getDescription())
.owner(user)
.eventDateTime(createEventRequest.getEventDateTime())
.location(createEventRequest.getLocation())
.eventType(createEventRequest.getEventType())
.build();
Event saved = eventRepository.save(event);
boolean alreadyExists = eventAttendeeRepository.existByEventAndUser(saved,user);
// 이미 존재하는지 여부 판단.
if(alreadyExists){
throw new BusinessException(ErrorCode.CREW_USER_DUPLICATED);
}
EventAttendee eventAttendee = EventAttendee.builder()
.event(event)
.user(user)
.build();
eventAttendeeRepository.save(eventAttendee);
return saved.getOwner().getName();
}
✅ 7. 테스트 코드 설계(단위,통합 테스트 기준의 사고)
테스트 코드의 경우 크게 단위 테스트 , 통합 테스트로 나뉩니다.
단위 테스트
현재 하려고 하는 목적은 eventService의 createEvent가 제대로 동작하는지 여부를 판단하는 것입니다.
기본적으로 Mokito를 이용한 단위테스트를 진행해 보겠습니다.
[1]
테스트는 기본적으로 사전에 필요한 입력값을 구성합니다. given 이라고도 부르며
저는 CreateEventRequest를 이용하여 테스트 데이터를 set 해주어 만들었습니다.
[2]
그리고 서비스에서 사용되는 로직들에 대해서 가짜 반환값을 가공합니다. 이를 이용하는 이유는 실제로
우리가 테스트 하려고하는 것은 eventService.createEvent 이기 때문에 안에 반환값에 대해서 시나리오를
구성하는 것 입니다. 시나리오 대로 구성했을때 실제 서비스의 return 값 대로 잘 나오는지 확인합니다.
Mockito.when(userService.findUserByEmail(createEventRequest.getUserEmail())).thenReturn(user);
단위 테스트 코드 및 확인 결과.
@ExtendWith(MockitoExtension.class)
public class EventServiceTest {
@InjectMocks
private EventService eventService;
@Mock private UserService userService;
@Mock private EventRepository eventRepository;
@Mock private EventAttendeeRepository eventAttendeeRepository;
@Test
void 이벤트_등록(){
//given
CreateEventRequest createEventRequest = new CreateEventRequest();
createEventRequest.setEventType(EventType.PERSONAL);
createEventRequest.setEventDateTime(LocalDateTime.now().plusDays(1)); // 내일 일정에 대해 test
createEventRequest.setTitle("내일 일정 테스트");
createEventRequest.setDescription("user가 일정을 등록한다.");
createEventRequest.setUserEmail("mjkim1201@naver.com");
Location location = new Location(
"스타벅스 강남점",
"서울특별시 강남구 테헤란로 123",
37.497942,
127.027636
);
createEventRequest.setLocation(location);
//가짜 유저 가정
User user = User.builder()
.id("12314324234")
.email("mjkim1201@naver.com")
.name("김러너")
.build();
Event event = Event.builder()
.id("123123324324234")
.title(createEventRequest.getTitle())
.eventType(createEventRequest.getEventType())
.eventDateTime(createEventRequest.getEventDateTime())
.owner(user)
.location(createEventRequest.getLocation())
.description(createEventRequest.getDescription())
.build();
// mock 지정
Mockito.when(userService.findUserByEmail(createEventRequest.getUserEmail())).thenReturn(user);
Mockito.when(eventRepository.save(Mockito.any(Event.class))).thenReturn(event);
Mockito.when(eventAttendeeRepository.existByEventAndUser(Mockito.any(), Mockito.any())).thenReturn(false);
Mockito.when(eventAttendeeRepository.save(Mockito.any(EventAttendee.class))).thenAnswer(inv -> inv.getArgument(0));
// when
String result = eventService.createEvent(createEventRequest);
// then
assertThat(result).isEqualTo("김러너");
}

'개발일지 > SPRINGBOOT' 카테고리의 다른 글
| Spring Data Envers와 RevisionRepository로 엔티티 변경 이력 관리하기 (0) | 2025.10.08 |
|---|---|
| Spring Boot에서 Swagger로 API 문서 자동화하기 (2) | 2025.08.02 |
| [SpringBoot] 전략 패턴으로 카카오, 네이버, 구글 소셜 로그인 유연하게 구현하기 (2) | 2025.06.14 |
| Builder 패턴을 적용한 엔터티 (2) | 2025.06.08 |
| JPA , 값 객체는 무엇이고 어떻게 활용하는가? (1) | 2025.06.04 |