[스프링 부트 어드민 페이지] 5. JPA 연관관계 & API 문서

2021. 5. 19. 15:59개발공부/[패스트캠퍼스] 스프링 부트 어드민 페이지 만들기


Github 코드

 

JPA 연관관계

세 번째 글에서 다룬 JPA 연관관계가 ORM(객체관계매핑)의 핵심이라서 백기선님 유튜브도 참고해 정리했습니다. @OneToMany의 mapped by를 함으로써 둘의 연관관계가 양방향이됩니다. 그리고 관계의 주인인 테이블은 'Many'가 되는 테이블 입니다. 아래 코드에선 Book 클래스가 주인 테이블 입니다.

백기선님 유튜브 : https://www.youtube.com/watch?v=hsSc5epPXDs

 

add(Book book) 메서드 내용에 양 테이블 변경이 포함되어야 합니다. 오류는 메서드의 두 번째 코드(bookStore 관계)만 작성되어 있고 book의 관계를 선언하지 않아 DB가 싱크로할 부분이 누락되었기 때문에 오류가 발생한다고 합니다.

@ToString(exclude={""}) 중복호출 제거

@ManyToOne, @OneToMany는 Lombok @ToString(exclude = {""})에서 제외시켜야 합니다. ToString은 클래스 객체의 필드값들을 출력해줍니다. 하지만 연관관계 어노테이션 필드는 양방향 관계에 속하기 때문에 양쪽에서 핑퐁처럼 계속 호출해주는 무한 루프에 빠지게 되므로 제외해줘야 합니다.

JPA ORM 연관관계의 장점

연관관계는 무엇보다 연관된 객체들을 통해 쿼리를 사용하지 않고도 DB 정보들을 간결한 코드로 조회할 수 있다는 장점이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//UserRepositoryTest
@Test
    @Transactional
    public void read() {
        User user = userRepository.findFirstByPhoneNumberOrderByIdDesc("010-1111-2222");
 
        user.getOrderGroupList().stream().forEach(orderGroup ->; {
 
            System.out.println("----------주문묶음---------------");
            System.out.println(orderGroup.getRevAddress());
            System.out.println(orderGroup.getTotalPrice());
            System.out.println(orderGroup.getTotalQuantity());
            System.out.println(orderGroup.getRevName());
            System.out.println("------------주문상세------------");
            orderGroup.getOrderDetailList().stream().forEach(orderDetail ->; {
                System.out.println("주문 상품: " + orderDetail.getItem().getName());
                System.out.println("고객센터 번호: " + orderDetail.getItem().getPartner().getCallCenter());
                System.out.println("주문의 상태 : " + orderDetail.getStatus());
                System.out.println("도착예정일자: " + orderDetail.getArrivalDate());
            });
        });
 
        Assertions.assertNotNull(user);
    }
cs

JPA 추가기능

DB컬럼의 디폴트값을 자동입력 해주는 기능을 AuditorAware(감시자) 인터페이스와 JPA 설정 클래스를 사용해 구현해줄 수 있습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
@EnableJpaAuditing //Jpa 감시 활성화
public class JpaConfig {
 
//---JPA 설정 클래스
 
 
@Component
public class LoginUserAuditorAware implements AuditorAware<String> {
 
    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of("AdminServer");
    }
//---객체별 Auditor 생성 클래스
 
@EntityListeners(AuditingEntityListener.class)
public class User {
 
    @CreatedDate
    private LocalDateTime createdAt;
    @CreatedBy
    private String createdBy;
    @LastModifiedDate
    private LocalDateTime updatedAt;
    @LastModifiedBy
    private String updatedBy;
 
//---Entity 클래스
 
 
 
 
cs

 

Lombok을 이용한 생성자 패턴

이펙티브 Java 책에서도 읽었던 내용인데 객체 생성자 패턴 중 정확성과 가독성이 높은 건 Lombok의 Builder() 패턴인 것 같습니다. 어노테이션만으로 간단히 설정 후 사용할 수 있는 @Builder는 새로운 필드가 추가되거나 삭제돼도 메서드로 필드를 자유롭게 주입시켜 객체를 생성할 수 있는 장점이 있습니다. 유사한 기능으로 Accessors(chain = true)도 있습니다. 차이는 update() 메서드를 사용할 때도 user.setEmail().setPhoneNumber()를 이어서 작성할 수 있다는 것입니다.

1
2
3
4
5
6
7
8
9
10
//Builder
User u = User.builder()
                .account(account)
                .password(password)
                .status(status)
                .email(email)
                .build();
 
//Accessors
u.setCreatedBy().setCreatedAt().setEmail();
cs

API 문서

프론트엔드 개발자와 공유하는 API 문서입니다. 문서는 개발 전 공유하는 것으로 다양한 툴이 있지만 버전 관리를 위해 Github과 연동은 공통적으로 필요한 부분입니다. 툴에 대해 궁금하시다면 잘 정리해둔 블로그를 참고하세요.

구글 스프레드시트 사용

기본적인 URL, Host, Method가 개요에 들어가고 주요기능 HTTP Request에 들어갈 파라미터나 Response로 돌려줄 데이터들에 대한 상세정보가 기재되어 있습니다. Response 헤더와 바디의 구분은 data만 바디, 나머지는 고정된 헤더값으로 나뉩니다. 


네트워크 관련 Entity(Header, Request, Response) 클래스 작성

JPA에서 DB와 Spring 클래스 변수의 Snake case <-> Camel case 자동 변환기능은 앞선 글에서 설명했습니다. 그러나 JSON 형식으로 된 Header의 변수를 Snake case에 맞춰 변경해주려면 두 가지 방법이 있습니다. 첫 번째는 @JsonProperty("[SNAKE CASE]") 어노테이션을 변경하려는 변수에 작성하는 방법이며 두 번째는 application.properties 파일에 설정 코드(spring.jackson.property-naming-strategy=SNAKE_CASE)를 추가해주는 것입니다. 


사용자 API 만들기

Controller 기본 CRUD 메서드들은 CrudInterface를 생성해 상속으로 강제해주면 편합니다. 팁인데, Interface는 다른 Controller들에서도 재사용이 가능하도록 제네릭 타입으로 만들어 줍니다.

Java Generic

1
2
3
4
5
6
public class Box {
    private Object object;
 
    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}
cs

 

클래스 인스턴스 변수가 Object 타입이면 어떤 타입을 제공하여도 받을 수 있습니다. 하지만 클래스에 제네릭을 작성해 원하는 타입의 변수만 들어올 수 있도록 만들어 줄 수 있는거죠. 프로젝트에선 newtwork 패키지의 Header<T> 클래스를 제네릭으로 선언해 private T data 인스턴스 변수 data 타입을 UserApiResponse로만 받도록 설정해 컴파일 오류에서 발견할 수 있도록 했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Header<T> {
 
    //api 통신시간
//    @JsonProperty("transaction_time")
    private LocalDateTime transactionTime; //기본적으로 프론트와 통신할 땐 시간도 String 타입을 씁니다.
    //api 응답코드
    private String resultCode;
    //api 부연설명
    private String description;
    //data
    private T data;
 
    //Methods
    //OK
    public static <T> Header<T> OK() {
        return (Header<T>) Header.builder()
                .transactionTime(LocalDateTime.now())
                .resultCode("OK")
                .description("OK")
                .build();
    }
cs

https://docs.oracle.com/javase/tutorial/java/generics/types.html

제네릭 타입별 컨벤션 입니다. 자료구조 책에서 Node<E>에 사용했었고, Map<K,V>를 사용해보았습니다. N은 아직 사용해보지 못했네요.

create API

Request 데이터를 받아줄 준비를 해야합니다. UserApiRequest, UserApiResponse 두 클래스를 생성해 같은 변수들을 처리해서 전송할 수 있도록 해줍니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Interface
public interface CrudInterface<Req, Res> {
    Header<Res> create(Req req); 
}
 
//Controller
@RestController
@RequestMapping("/api/user")
public class UserApiController implements CrudInterface<UserApiRequest, UserApiResponse> {
        
@Override
    @PostMapping("")
    public Header<UserApiResponse> create(@RequestBody UserApiRequest userApiRequest) {
        return null;
    }
}
cs

출처 : 패스트캠퍼스 Java 웹 개발 마스터 올인원 패키지