이번 포스트에서는 본격적으로 서버 애플리케이션을 작성할 예정입니다. 매우 간단하고, 있을거는 최소한으로 갖춘 채로 작성할 예정이니 천천히 따라오세요!
프로젝트 폴더에 다음과 같이 여러 패키지를 생성합니다.
그 다음 단계로, result package 내부의 클래스들을 모두 작성합시다.
ResultProvider의 경우 리턴 포맷을 뱉어내는 역할을 하는 클래스라고 보면 되겠습니다. ResultProvider를 작성하기 이전에 CommonResult, SingleResult, MultipleResult를 먼저 작성해봅시다.
CommonResult.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult {
private Boolean success;
}
SingleResult.java
@Getter
@Setter
@NoArgsConstructor
public class SingleResult<T> extends CommonResult {
private T data;
}
MultipleResult.java
@Getter
@Setter
@NoArgsConstructor
public class MultipleResult<T> extends CommonResult {
private List<T> data;
}
위에 나열한 클래스들을 대충 설명해주겠습니다.
위의 클래스를 이용해서, ResultProvider를 작성해주자.
ResultProvider.java
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ResultProvider {
// 성공 결과만 반환하는 메소드
public static CommonResult getSuccessResult() {
return new CommonResult(true);
}
// 단일 데이터를 가지는 성공을 반환하는 메소드
public static <T> SingleResult<T> getSingleResult(T data) {
SingleResult<T> result = new SingleResult<>();
result.setData(data);
result.setSuccess(true);
return result;
}
// 리스트 형태의 데이터를 가지는 성공을 반환하는 메소드
public static <T> MultipleResult<T> getMultipleResult(List<T> data) {
MultipleResult<T> result = new MultipleResult<>();
result.setData(data);
result.setSuccess(true);
return result;
}
}
ResultProvider 클래스에 대해서 설명을 해주겠습니다.
일단 ResultProvider class 상단에 어노테이션으로 @NoArgsConstructor(access = AccessLevel.PRIVATE) 를 달아준 모습을 확인할 수 있는데, 생성자의 액세스 접근 권한을 private로 제한을 둠으로써 해당 ResultProvider class의 인스턴스화를 막아줍니다.
그리고 static method를 이용하여 CommonResult, SingleResult, MultipleResult를 반환하는 메소드를 각각 작성한다. 따라서 ResultProvider를 이용해서 리턴을 뱉어내면 다음의 형식을 따를것이다.
⚡️SingleResult를 통해서 발생한 return 예시
{
"success" : true,
"data" : {
"id" : 1,
"name" : "테스트 맛집"
"address" : "서울시 강남구 강남역 10번 출구"
}
}
다음으로, Entity들을 작성해주자.
이전에 사용했던 그림을 가져오겠습니다.
위의 그림을 다시 보게되면, shop과 menu는 서로 1:N의 연관관계를 가지고 있습니다. 따라서 저희가 작성할 Entity는 해당 연관관계까지 함께 작성을 해주도록 하겠습니다.
우선, Shop class를 먼저 확인하겠습니다.
Shop.java
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Accessors(chain = true)
@Entity
public class Shop {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String address;
// Shop : Menu = 1 ; N
@OneToMany(fetch = FetchType.LAZY, mappedBy = "shop", cascade = CascadeType.ALL)
@JsonManagedReference
private List<Menu> menuList;
}
각각의 어노테이션을 설명드리겠습니다.
그리고 연관관계 매핑 부분에서, @OneToMany에 지연로딩 속성을 걸어둔 것을 확인할 수 있습니다. 물론 @OneToMany의 기본 로딩 전략이 지연로딩이기는 하지만... 명시적 목적으로 일단 적어두었습니다.
JPA의 연관관계에는 지연로딩 방식, 즉시로딩 방식이 존재합니다. 즉시로딩 방식은 데이터를 불러오는 속도가 빠르다는 장점이 존재하지만, 즉시로딩에 관련된 모든 엔티티를 조회하고, 그리고 그 엔티티들을 모두 따로 조회하기 때문에 쿼리가 비정상적으로 많이 실행되는 문제를 야기합니다.
(흔히, JPA의 N+1 문제라고도 불리는 문제죠)
따라서, JPA에서 연관관계를 사용할 때에는 무조건 지연로딩으로 사용하는 것을 추천합니다.
그리고 cascade 속성을 부여한 것을 확인할 수 있는데, cascade 속성을 부여함으로써 데이터베이스에서 발생할 수 있는 결함을 방지해주는 효과를 가져올 수 있습니다.
그리고, @JsonBackReference, @JsonManagedReference 어노테이션에 대해서 설명을 하겠습니다. 위의 두 어노테이션은 Entity를 조회하는데 있어서 순환참조 를 방지하기 위해 Json을 생성할 때에 있어서 어느 엔티티가 연관관계에 있어서 주인의 역할을 수행하는지 명시하는 역할을 수행합니다. 보통은 1:N의 관계에서 N이 연관관계의 주인이고, 1이 연관관계의 노예(?)라고 생각하시면 될듯합니다. 표현이 이게 맞나?
이제 Menu class를 확인해보겠습니다.
Menu.java
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Accessors(chain = true)
@Entity
public class Menu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Menu : Shop = N : 1
@ManyToOne
@JsonBackReference
private Shop shop;
}
이제, Repository를 작성하겠습니다.
Repository는 코드만 제시하겠습니다. 간단하니까요.
MenuRepository.java
@Repository
public interface MenuRepository extends JpaRepository<Menu, Long> {
}
ShopRepository.java
@Repository
public interface ShopRepository extends JpaRepository<Shop, Long> {
}
어차피 기본으로 제공되는 Repository 메소드만을 사용할 생각이라... 내부적으로 쿼리메소드를 작성하지 않았습니다.
위의 과정들을 잘 따라오셨다면, 아래의 그림과 같이 프로젝트가 구성이 되어있을겁니다.
이 포스트에서 작성하지 않은 것 같은 클래스들도 있긴하지만...신경 안 쓰셔도 됩니다!
지금까지 프로젝트의 아키텍처 구성, 그리고 Entity, Repository까지 작성 완료하였습니다. 다음 포스트에서는 Controller, Service, dto를 채워넣겠습니다.
다음 포스트에서 뵙겠습니다!