2024. 2. 21. 00:38ㆍWeb/Spring
[Spring] Category 구현하기
쇼핑몰, 커뮤니티에 존재하는 주제를 표현할 때 자주 사용되는 것이 카테고리이다. 카테고리는 전형적인 계층형으로 서로 연관이 되는 구조이다. 해당 카테고리만을 단순히 구현해 보고 구현을 할 때 과정을 정리하려고 한다.
코드
먼저 코드들은 다음과 같다. 좀 더 구체화를 시키기 위해서는 인터페이스를 사용하고 DI를 사용하는 등의 과정을 거쳐야하는 것이 맞지만 현재의 경우 카테고리를 구현하는 것이 목표였기에 간단하게 컨트롤러, 엔티티, 서비스, 레포지토리 4개의 구성 요소만을 사용해서 진행을 하였다. 후에 다른 프로젝트를 진행할 때는 해당 부분에 대해 자세하게 세분화하고 DI를 지키는 식으로 진행을 할 예정이다.
Controller
package com.example.categorytemp.domain;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/categories")
@RequiredArgsConstructor
public class CategoryController {
private final CategoryService categoryService;
@GetMapping
public List<Category> findAll() {
return categoryService.findAll();
}
@GetMapping("/{id}")
public Category findById(@PathVariable Long id) {
return categoryService.findById(id);
}
@PostMapping
public Category save(@RequestBody Category category) {
return categoryService.save(category);
}
@GetMapping("/code/{code}")
public Category findByCode(@PathVariable String code) {
return categoryService.findByCode(code);
}
}
CategoryService
package com.example.categorytemp.domain;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CategoryService {
private final CategoryRepository categoryRepository;
public List<Category> findAll() {
return categoryRepository.findAll();
}
public List<Category> findByParentId(Long parentId) {
return categoryRepository.findByParentId(parentId);
}
public Category save(Category category) {
return categoryRepository.save(category);
}
public Category findByCode(String code) {
return categoryRepository.findByCode(code).orElse(null);
}
public Category findById(Long id) {
return categoryRepository.findById(id).orElseThrow(()->new IllegalArgumentException("잘못된 접근입니다"));
}
}
CategoryRepository
package com.example.categorytemp.domain;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
List<Category> findByParentId(Long parentId);
Optional<Category> findByCode(String code);
}
카테고리 - Category
package com.example.categorytemp.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "category")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String code;
@ManyToOne
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
@JsonIgnore
private List<Category> children;
}
카테고리의 경우 JPA에서의 엔티티 객체이며 서로가 서로를 참조하는 구조를 가지고 있다. 그렇기에 이와 관련이 된 어노테이션들에 대해 기록을 하려고 한다.
- @ManyToOne
- 다대일 관계를 위해 사용한다.
- Category의 경우 하나의 부모 카테고리(parent)를 가질 수 있지만 여러 자식 카테고리(children)을 가질 수 있다.
- @JoinColumn(name ="parent_id")
- 다대일 관계에서 외래 키 매핑을 지정한다
- 현재 엔티티의 "parent_id" 컬럼이 부모 카테고리 테이블의 주요 식별자 칼럼과 연결된다
- @OneToMany(mappedBy = "parent")
- 다대일 관계에서 외래 키 매핑을 지정한다
- 여러 자식 카테고리(children)를 가질 수 있지만 자식 카테고리는 하나의 부모 카테고리만 가질 수 있다.
- mappedBy 속성은 자식 카테고리 엔티티에서 매핑된 필드 이름을 지정한다
- @JsonIgnore
- Jackson 라이브러리를 사용하여 JSON으로 변환할 때 해당 필드를 무시하도록 지정한다
- 해당 Children 필드는 Json 데이터에 포함되지 않는다
- 해당 어노테이션으로 해당 필드를 무시하도록 설정을 하였다. 이를 통해 List에서 서로가 서로를 참조하여 순환이 되는 상황을 방지하였다. 대신 계층 구조는 parent부분이 보이면서 계층을 표현하게 되었다.
카테고리를 진행하며 발생한 순환 오류는 다음과 같다. 오류가 반복적으로 작성이 사용이 되었기에 일부분만 발췌 하였다.
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serialize AsField(BeanPropertyWriter.java:732) ~[jackson-databind-2.15.3.jar:2.15.3] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:772) ~[jackson-databind-2.15.3.jar:2.15.3] at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178) ~[jackson-databind-2.15.3.jar:2.15.3] at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serializeContents(CollectionSerializer.java:145) ~[jackson-databind-2.15.3.jar:2.15.3] at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:107) ~[jackson-databind-2.15.3.jar:2.15.3] at com.fasterxml.jackson.databind.ser.std.CollectionSerializer.serialize(CollectionSerializer.java:25) ~[jackson-databind-2.15.3.jar:2.15.3] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:732) ~[jackson-databind-2.15.3.jar:2.15.3]
그렇기에 위의 JsonIgnore을 사용하여 순환 참조되는 부분을 끊어주는 식으로 구현을 하였고 실제 데이터 베이스에는 다음과 같이 저장이 되어있다. 그리고 포스트맨에서의 테스트를 통해서는 다음과 같이 보인다.
물론 DTO를 사용하여 구현을 하면 순환관계가 발생하는 해당 엔티티를 그대로 사용하는 것이 아니기에 순환에서의 오류가 안 생겼을 수도 있다. 그러나 엔티티를 반환 값으로 사용하기에 순환 오류가 발생하였고, 이를 해결하기 위해 알게 된 JsonIgnore 어노테이션을 찾아보고 사용할 수 있었다.
환경세팅
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
applicaiton.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/{데이터베이스 schema 이름}?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 1234
jpa:
hibernate:
ddl-auto: create
'Web > Spring' 카테고리의 다른 글
[Spring] HTTPS 적용하기 (0) | 2024.02.17 |
---|---|
[Spring] 휴대폰 인증 - 네이버 sens 사용 (0) | 2023.08.26 |
[Spring] 이메일 인증 (0) | 2023.08.26 |
[Error] java.lang.ClassNotFoundException: org.apache.hc.client5.http.classic.HttpClient (0) | 2023.08.17 |
[Etc] application.yml 변경 및 Jasypt (0) | 2023.08.09 |