파일 구조
📦 com.example.schedule
│
├── controller
│ ├── HomeController --> 🧑 HTML 뷰 렌더링 (홈, 회원가입, 로그인, 일정 등록, 일정 목록)
│ ├── UserController --> 📦 회원가입, 유저 수정 컨트롤러
│ ├── AuthController --> 로그인/로그아웃(인증 관련) 컨트롤러
│ ├── CommentController --> 댓글 관련 컨트롤러
│ ├── ApiUserController --> 📦 REST API (유저 관련 JSON 처리)
│ ├── ScheduleController --> 📦 REST API (일정 관련 JSON 처리)
│
├── dto
│ ├── UserRequestDto --> 회원가입/로그인 요청 DTO
│ ├── UserResponseDto --> 유저 응답 DTO (API용)
│ └── Schedule DTO들 ...
│
├── domain
│ ├── User --> 회원 Entity
│ ├── Schedule --> 일정 Entity
│ └── BaseTimeEntity --> 생성/수정일 공통 Entity
│
├── service
│ ├── UserService --> 회원 비즈니스 인터페이스
│ ├── UserServiceImpl --> 회원 비즈니스 구현체
│ ├── ScheduleService --> 일정 인터페이스
│ └── ScheduleServiceImpl --> 일정 구현체
│
├── repository
│ ├── UserRepository
│ └── ScheduleRepository
│
├── config
│ ├── WebConfig --> 필터 등록 등 설정
│ └── AuthFilter --> 인증 필터 (세션 검사)
│
├── exception
│ ├── CustomException
│ ├── ErrorCode
│ └── GlobalExceptionHandler
│
└── templates
├── home.html --> 홈 페이지
├── login.html --> 로그인 폼
├── register.html --> 회원가입 폼
└── schedule/ --> 일정 관련 페이지들
1. 엔티티 구현
- User, Schedule 엔티티 정의
- 공통된 시간 필드는 BaseTime 엔티티로 구현해서 분리
- ( 할 일 같은 경우엔 작성자, 수정자도 언젠간 필요할 듯 하여 Base 엔티티 상속 )
- JPA Auditing 설정
- 연관관계는 단방향, (Schedule : N -> User : 1, 연관관계의 주인은 FK를 가진 곳)
Schedule 엔티티
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Schedule extends BaseTime {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "schedule_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
// 생성자
private Schedule(User user, String title, String content) {
this.user = user;
this.title = title;
this.content = content;
}
// 정적 팩토리 메서드
public static Schedule of(User user, String title, String content) {
return new Schedule(user, title, content);
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
- 스프링부트에선 @Builder나 생성자를 따로 설정해주는데, @NoArgsConstructor은 또 뭘까?
- AccessLevel은 왜 PROTECTED일까?
@NoArgsConstructor는 기본 생성자를 뜻하는 것이고, 해당 생성자의 access level을 protected로 설정한다는 뜻이다.
그럼 엔티티에서 왜 이 어노테이션을 사용할까?
결론부터 말하면 엔티티의 Proxy 조회 때문이다.
엔티티의 연관 관계가 맺어져있는 객체를 조회할 때 지연 로딩의 경우엔 실제 엔티티가 아닌 객체의 참조값을 가지는 프록시 객체를 조회 한 후, 해당 객체 데이터를 호출했을 때 프록시 객체가 해당 객체의 참조값을 통해 조회한다.
...?
아직까지는 이해하기 어렵다.. 나중에 자세히 공부를 해봐야 할 것 같다.
@ManyToOne(fetch = FetchType.LAZY)
연관 관계에서 즉시 로딩과 지연 로딩
즉시로딩은 JPQL에서 N+1 문제를 일으켜 실무에서 쓰지 않는다고 하였고 발생하는 오류 중에서 즉시 로딩 때문에 문제가 있었다~ 라는 걸 강의에서 봐서 일단은 지연 로딩을 사용했다. 이 부분에 대해서도 자세히 공부해봐야 할 것 같다.
정적 팩토리 메서드(Static Factory Method)
객체를 생성할 때, 생성자를 쓰지 않고 정적 메서드를 사용하는 것이다. 생성자의 접근 제어자가 public인 경우, 생성자를 통해 객체 생성을 언제 어디서든 제한없이 할 수 있게 되고, 어떤 인스턴스를 반환한 것인지 제어할 수 없다.
정적 팩토리 메서드를 사용하면 객체 생성을 자기 자신이 관리할 수 있다.
@Builder가 JPA 지연 로딩이나 기본 생성자와 충돌날 수 있어 정적 팩토리 메서드를 사용했는데 솔직히 말하면 아직 이해가 안가는 부분이 많다.
싱글톤 패턴, 지연 로딩, reflection 등등 다양한 키워드가 나와서 다음주에 시간내고 쫙 공부해야겠다.. 이해가 안가는 것 투성이다.
사실 이번에 테스트 코드도 조금 작성해봤다. JPA를 처음 사용해본 입장으로써 나는 SQL 을 작성하지 않았는데 JPA 내부에서
Entity를 사용하면 매핑이 되는 걸까? 이해가 안가서 어떻게 동작하는지 알고 싶었다.
따라서 Entity 단위로 테스트하는 테스트 코드를 작성 후 테스트를 해봤다.
@PersistenceContext
EntityManager em;
@Test
public void testEntity() {
User user1 = User.of("admin", "admsadadasin", "admsain@gmail.com");
em.persist(user1);
Schedule schedule1 = Schedule.of(user1, "회의", "회의를 합니다" );
em.persist(schedule1);
em.flush();
em.clear(); //1차 캐시 제거,
Schedule sc = em.find(Schedule.class, schedule1.getId());
String username = user1.getUsername();
assertThat(username).isEqualTo(user1.getUsername());
}
설정 | 이유 |
@Transactional | 트랜잭션 유지 → LAZY 로딩 가능 |
em.persist(...) | EntityManager를 통해 직접 저장, "이 엔티티를 저장할거야" 라고 JPA에게 알려줌 |
em.flush() | DB에 강제로 반영, "지금까지 등록된 변경 내용을 진짜 DB에 저장해" |
em.clear() | 1차 캐시 제거 → 영속성 컨텍스트를 초기화해! |
...
'TIL' 카테고리의 다른 글
TIL21 - 뉴스피드 프로젝트(엔티티 생성, 연관관계 설정), 작성중...... (2) | 2025.04.09 |
---|---|
TIL20 - 뉴스피드 프로젝트(깃 컨벤션, 이슈, 브랜치) (4) | 2025.04.09 |
📅 일정 관리 애플리케이션(도전과제3) (0) | 2025.03.25 |
📅 일정 관리 애플리케이션(필수과제) (0) | 2025.03.23 |
TIL13. 키오스크 도전과제2 (1) | 2025.03.14 |