Back-End/Spring (SpartaCC)

[Spring] 스파르타코딩클럽 Spring공부(5) - Validation, Entity 관계

남건욱 2023. 6. 23. 04:22
반응형
Validation
Spring에서는 null 확인 뿐 아니라 문자의 길이 측정과 같은 다른 검증 과정도 쉽게 처리할 수
있도록 Bean Validation을 제공하고 있다.

- 간편하게 사용할 수 있는 여러 애너테이션을 제공해 준다.

 

@NotNull - null 불가

@NotEmpty - null, "" 불가

@NotBlank - null, "", " " 불가

@Size - 문자 길이 측정

@Max - 최댓값

@Min - 최솟값

@Positive - 양수

@Negative - 음수

@Email - E-mail 형식

@Pattern - 정규 표현식

implementation 'org.springframework.boot:spring-boot-starter-validation'

위 코드를 build.gradle에 추가하여 사용할 수 있다.

 

 

 

 

 

RestTemplate
Spring에서 지원하는 객체로 간편하게 Rest방식 API를 호출할 수 있는 Spring 내장 클래스

- 지금까지는 Cline 즉, 브라우저부터 요청을 받는 서버의 입장에서 개발을 진행했다. 서비스 개발을 진행하다 보면 라이브러리 사용만으로는 구현이 힘든 기능들이 많다. 예를 들어 우리의 서비스에서 회원가입을 진행할 때 사용자의 주소를 받아야 한다면, 이것을 직접 구현하게 되면 많은 시간과 비용이 들어갈 것이다. 이때 카카오에서 만든 주소 검색 API를 사용한다면 간편하게 구현할 수 있을 것이다.

 

 

 

 

 

RestTemplate의 Get요청
private final RestTemplate restTemplate;

// RestTemplateBuilder의 build()를 사용하여 RestTemplate을 생성한다.
public RestTemplateService(RestTemplateBuilder builder) {
    this.restTemplate = builder.build();
}

-  먼저 위와 같이 RestTemplate을 주입받는다.

 

 

public ItemDto getCallObject(String query) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/get-call-obj")
            .queryParam("query", query)
            .encode()
            .build()
            .toUri();
    log.info("uri = " + uri);

    ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);

    log.info("statusCode = " + responseEntity.getStatusCode());

    return responseEntity.getBody();
}

- Spring의 UriComponentsBuilder를 사용해서 URI를 손쉽게 만들 수 있다.

- RestTemplate의 getForEntity는 Get 방식으로 해당 URI의 서버에 요청을 진행한다.

- 요청의 결괏값에 대해 직접 JSON TO Object를 구현할 필요 없이 RestTemplate을 사용하면 자동처리하여 준다.

 

 

 

 

 

RestTemplate의 Post요청
public ItemDto postCall(String query) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/post-call/{query}")
            .encode()
            .build()
            .expand(query)
            .toUri();
    log.info("uri = " + uri);

    User user = new User("Robbie", "1234");

    ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);

    log.info("statusCode = " + responseEntity.getStatusCode());

    return responseEntity.getBody();
}

- 요청받은 검색어를 Query String 방식으로 서버로 RestTemplate을 사용하여 요청한다.

- UriComponentsBuilder의 expend를 사용하여 {query} 안의 값을 동적으로 처리할 수 있다.

- RestTemplate의 postForEntity는 Post방식으로 해당 URI의 서버에 요청을 진행한다.

- 첫 번째 피라미터에는 URI, 두 번째 피라미터에는 HTTP Body에 넣어줄 데이터를 넣는다. 세 번째 피라미터에는 전달받은 데이터와 매핑하여 인스턴스화할 클래스의 타입을 넣어주면 된다.

 

 

 

 

 

 

RestTemplate의 exchange
public List<ItemDto> exchangeCall(String token) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/exchange-call")
            .encode()
            .build()
            .toUri();
    log.info("uri = " + uri);

    User user = new User("Robbie", "1234");

    RequestEntity<User> requestEntity = RequestEntity
            .post(uri)
            .header("X-Authorization", token)
            .body(user);

    ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

    return fromJSONtoItems(responseEntity.getBody());
}

요청 Header에 정보를 추가하려면 어떻게 해야 할까?

- exchange 메서드의 첫 번째 피라미터에 RequestEntity 객체를 만들어 전달해 주면 uri, header, body의 정보를 한 번에 전달할 수 있다.

 

 

 

 

 

 

 

DB table 간의 방향

- 방향은 크게 단방향과 양방향을 생각할 수 있다.

테이블이 users, food 두 개의 테이블이 있다고 가정을 했을 때, 단방향은 users테이블에서만 food테이블을 참조할 수 있을 때를 의미한다. 양방향은 users테이블과 food테이블이 서로를 참조할 수 있을 때를 의미한다. 그러면 DB 테이블가느이 방향이 있는 게 맞을까? DB에서는 어떤 테이블을 기준으로 하든 원하는 정보를 JOIN 하여 조회할 수 있다. 이처럼 DB 테이블 간의 관계에는 방향의 개념이 없다.

 

 

 

 

 

Entity 간의 연관 관계

- DB테이블에서는 테이블 사이의 연관관계를 FK(외래 키)로 맺을 수 있고, 방향 상관없이 조회가 가능하다.

- Entiy에서는 상대 Entity를 참조하여 Entity사이의 연관관계를 맺을 수 있다.

- 상대 Entity를 참조하고 있지 않다면 상대 Entity를 조회할 수 있는 방법은 없다.

- Entity에서는 DB테이블에는 없는 방향의 개념이 존재한다.

 

 

 

 

 

1대 1 단방향 관계

- 외래키의 주인 정하기 : Entity에서 외래 키의 주인은 일반적으로 N(다)의 관계는 Entity지만 1대 1 관계에서는 외래 키의 주인을 직접 지정해야 한다.

- 외래키 주인만이 외래키를 등록, 수정, 삭제할 수 있으며 주인이 아닌 쪽은 오직 외래키를 읽기만 가능하다.

- @JoinColumm()은 외래키의 주인이 화용 하는 애너테이션이다. (칼럼명, null여부, unique 여부등을 지정할 수 있다.)

 

// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

 

 

 

 

 

1대 1 양방향 관계

- 양방향 관계에서 외래키의 주인을 지정해 줄 때 mappedBy 옵션을 사용한다.

- mappedBy의 속성값은 외래키의 주인인 상대 Entity의 필드명을 의미한다.

- 외래키의 주인은 상대 Entity타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래키의 속성을 설정해 주면 된다.

// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne(mappedBy = "user")
    private Food food;
}

 

 

 

 

 

N대 1 단방향 관계
// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

- @ManyToOne 애너테이션은 N대 1 관계를 맺어주는 역할을 한다. 

 

 

 

 

 

N대 1 양방향 관계
// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}


// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Food> foodList = new ArrayList<>();
}

- 양방향 참조를 위해 고객 Entity에서 Java컬렉션을 사용하여 음식 Entity를 참조하는 코드이다.

 

 

 

 

 

1대 N 단방향 관계
// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany
    @JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
    private List<User> userList = new ArrayList<>();
}

// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

- @OneToMany 애너테이션은 1대 N관계를 맺어주는 역할을 한다.

- 위코드에서 외래키를 관리하는 주인은 음식 Entity이지만 실제 외래키는 고객 Entity가 가지고 있다. 

- 1:N에서 N관계의 테이블이 외래키를 가질 수 있기 때문에 외래키는 N의 관계인 users테이블에 키 칼럼을 만들어 추가하지만 외래키의 주인인 음식 Entity를 통해 관리한다.

- 외래키를 음식 Entity가 직접 가질 수 있다면 INSERT발생 시 한 번에 처리할 수 있지만, 실제 DB에서 외래키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재한다.

 

 

 

 

 

1대 N 양방향 관계

- 1대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않는다.

- 1대N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy옵션을 사용해야지만 @ManyToOne 애너테이션은 mappedBy속성을 제공하지 않는다.

 

 

 

 

N대 M 단방향 관계
// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
    joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
    inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();
}

// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

- @ManyToMany 애너테이션은 N대 M 관계를 맺어주는 역할을 한다.

- 생성되는 중간테이블을 컨트롤하기 어렵기 때문에 추후에 중간 테이블의 변경이 발생할 경우 문제가 발생할 가능성이 높다.

 

 

 

 

 

N대 M 양방향 관계
// 음식 (외래키의 주인)
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
    joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
    inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();
}

// 고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "userList")
    private List<Food> foodList = new ArrayList<>();
}

- 반대 방향인 고객 Entity에 @ManyToMany로 음식 Entity를 연결하고 mappedBy 옵션을 설정하여 외래 키의 주인을 설정하면 양방향 관계 맺음이 가능하다.

 

 

 

 

 

정리

Validation을 사용하면 받아올 입력값의 범위를 개발자가 지정한 범위 내에서 받아올 수 있다는 것을 알게 되었다. 하나씩 지정해줘야 하는 줄 알았는데 내가 몰랐던 애너테이션이 존재했었다. 앞으로 많이 활용될 것 같다. 또한 RestTemplate을 사용하여 간편하게 Rest방식 API를 호출해 코딩에 사용할 수 있다는 것을 알았다. 1대 1, 1대 N, N대 1, N대 M 관계에 대해서는 생각보다 개념이 헷갈렸다. 1대 1, N대 M방식은 어느 정도 이해가 되었고, 1대 N과 N대 1 상황에서의 애너테이션 종류와 요구되는 입력값을 제대로 이해해야 할 필요성을 느꼈다. 오늘의 공부는 코딩에 직접적으로 도움이 되는 공부들을 하게 됐던 것 같다. 자바스프링은 편한 기능이 많이 탑재되어 있는 것 같다. 

 

 

반응형
프로필사진

남건욱's 공부기록