|  Validation

- 지난 번 스프링을 처음 배울 당시 아래와 같이 Validation에 대한 개념을 배웠었다.

✅ Validation은 유효성 검증을 말하며, 주로 HTTP Request에서 잘못된 내용을 검증할 때 사용한다.
- 데이터 검증     : 필수 데이터 / 문자열 길이 및 숫자형 데이터 범위 / 이메일 및 신용카드 번호 등 형식 확인
- 비즈니스 검증 : 서비스 정책에 따라 데이터 검증
✅ Validation 방식
1) Java Bean Validation : dto클래스 맴버에 Annotaion(ex. @NotBlank, @Size, @Email...)을 붙이는 방식
2) Spring validator 인터페이스 구현을 통한 validation 
✅ Validation 주의사항 : Validation을 멀리 두면 테스트 및 유지보수성이 떨어지므로 가능한 Annotaion방식을 쓰는 게 좋다.
* 수업에서의 제안 : 1차로 dto에 Annotation방식 사용, 2차로 비즈니스 검증 후 실패 시 Custom Exception throw

- 오늘은 계좌 프로그램의 계좌 생성 기능에 대한 단위 테스트를 하면서 위 데이터를 검증하고, 비즈니스 로직을 검정하는 실습해보았다.

- 이것을 연습 삼아서 동물 병원 차트를 생성하는 걸로 바꿔서 해보았다.

 

1.  데이터 검증하기 - Common Validation Annotations(Java Bean Validation)

  • @NotNull: to say that a field must not be null.
  • @NotEmpty: to say that a list field must not empty.
  • @NotBlank: to say that a string field must not be the empty string (i.e. it must have at least one character).
  • @Min and @Max: to say that a numerical field is only valid when it’s value is above or below a certain value.
  • @Pattern: to say that a string field is only valid when it matches a certain regular expression.
  • @Email: to say that a string field must be a valid email address.

    [출처] https://reflectoring.io/bean-validation-with-spring-boot/

- 수업에서는 HTTP 요청/응답 데이터를 검증할 때, DTO객체에서 Java Bean Validation을 썼다.

- 연습 삼아 동물 병원 차트를 통해 정리해보았다.

- 아래는 동물 병원 차트 도메인 객체

// import 생략

// Lombok getter/setter/생성자 생략
@Entity
@EntityListener(AuditingEntityListener.class)
public class Chart{

    @Id
    @GeneratedValue   // sequence (auto-increment)
    private Long id;
    
    private String chartNo;   // 차트번호 (ex. AA20221211 - 종분류+생성일자)

    @ManyToOne
    private Owner owner;      // 주인
    private Patient patient;  // 환자 기본 정보 (이름, 나이, 성별 등)
    private VaccineRecord vaccineRecord; // 백신 여부
    
    private String memo;      // 증상 메모
    private String diagnose;  // 진단명 
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

[1] DTO : Request/Response Data-binding용 객체에 제약조건 걸기

[ 예시 ] 동물 병원 차트 생성 DTO - Request와 Response 시 Data-binding용
- 요청시 : 주인 id와 동물 id는 반드시 받아 존재하는지 확인한다. / 차트에 메모와 진단이 있을 수도 있고 없을 수도 있다.
- 응답시 : 생성된 차트의 차트번호 반환
// import 생략

public class CreateChart{

   // lombok 게터/세터/생성자 생략
   public static class Request{
    	@NotNull
        @Min(1)
    	private Long ownerId;
        @NotNull
        @Min(1)
        private Long patientId;
        private String memo;
        private String diagonose;
   }
   
   // lombok 게터/세터/생성자 생략
   public static class Response{
        @NotNull
        private String chartNo;
   }
   
   public static Response from(ChartDto chartDto){
        return Response.builder()
        	.ownerId(chartDto.getOwnerId())
        	.patientId(chartDto.getPatientId())
        	.memo(chartDto.getMemo())
        	.diagonose(chartDto.getDiagnose())
        	.build();
   }

}

 

[2] DTO :  도메인에서 필요한 데이터만 넘겨주는 객체 생성

* 지연 로딩을 위해서 필요한 객체이다. (지연 로딩이란? JPA에서 프록시 객체 - 참조값을 가진다 - 로 Entity간 연관관계를 주입할 때, 한 번에 모든 데이터를 올리는 것이 아니라 필요에 따라 지연하여 데이터를 로딩하는 방식)

* 지연 로딩을 하게 되면 사용자의 요청사항에 유연하게 반응할 수 있다.

// import 생략

// lombok 관련 annotation 생략
public class ChartDto {
    private String chartNo;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    public static ChartDto fromEntity(Chart chart){
    	return ChartDto.builder()
            .chartNo(chart.getChartNo())
            .createdAt(chart.getCreatedAt())
            .updatedAt(chart.getUpdatedAt())
            .build();
    }
}

 

[3] Controller에서 @Valid로 요청 시 데이터 제약사항 검증하기

[ 예시 ] 동물 병원 차트 생성 컨트롤러 - REST 방식의 컨트롤러

POST http://ip주소:port번호/chart
content-type: application/json

{
        "ownerId": 1,
        "name": "여름이"
}
// import 생략

@RestController
@RequiredArgsConstructor
public class ChartController{
    private final ChartService chartService;
    
    @PostMapping("/chart")
    public CreateChart.Response createChart(@RequestBody @Valid CreateChart.Request request){
    	
        return CreateChart.Response.from(
        	chartService.createChart(
        		request.getOwnerId(),
        		request.getPatientId(),
        	)
        );
        
    }
}

 

2.  비즈니스 로직 검증하기 - Custom Exception

[1] exception : 예외사항에 대한 Custom Exception을 만든다.

(1) 먼저 Enum으로 예외 코드를 만든다.
(2) 해당 Enum을 담은 커스텀 Exception을 만든다.
// import 생략

@Getter
@AllArgsConstructor
public Enum ErrorCode {
    OWNER_NOT_FOUND("주인이 없습니다."),
    PATIENT_NOT_FOUND("환자가 없습니다."),
    CHART_NO_NOT_FOUND("차트번호를 찾을 수 없습니다.")
    
    private String description;
}
// import 생략

// lombok 생략
public class ChartException extends RuntimeException {
    ErrorCode errorCode;
    String errorMessage;
    
    public ChartException(ErrorCode errorCode) {
    	this.errorCode = errorCode;
        this.errorMessage = errorCode.getDescription();
    }
}

 

[2] service : 비즈니스 로직에 따라 서비스 객체 생성하기

[ 예시 ] 동물 병원 차트 생성 서비스 - 비즈니스 로직 검증
- 주인이 있는지 조회하기 
- 동물이 있는지 조회하기
- 동물의 차트가 100개를 넘어가는지 확인 (*validate를 위해 임의로 만든 것)
- 최신 차트 번호 생성
- 차트 저장 후, 차트 정보를 넘긴다.
// import 생략

@Service
@RequiredArgsConstructor
public class ChartService {
    private final ChartRepository chartRepository;
    private final OwnerRepository OwnerRepository;
    private final PatientRepository PatientRepository;
    
    @Transactional
    public ChartDto createChart(Long ownerId, Long patinetId){
    	Onwer owner = ownerRepositroy.findById(ownerId)
        	.orElseThrow(() -> new ChartException(ErrorCode.OWNER_NOT_FOUND));
            
        Patient patient = patientRepository.findById(patientId)
        	.orElseThrow(() -> new ChartException(ErrorCode.PATIENT_NOT_FOUND));
            
        // 차트 100개 넘는지 별도로 validate 처리
        // -- [ctrl]+[alt]+[M]으로 메소드를 extract
        validate(patient);  
        
        String chartNo = chartRepository.findFirstByOrderByIdDesc()
        	.map(chart -> (Integer.parseInt(chart.getChartNo().substring(2) + 1)) + "")
        	.orElseThrow(() -> new ChartException(ErrorCode.CHART_NO_NOT_FOUND));
        
        return ChartDto.fromEntity(
        	chartRepository.save(
            	Chart.builder()
                	//생략
                	.build()
            )
        );        
    }
    
    public void validate (Patient patient) {
    	if (chartRepository.CountByPatient(patient) == 100) {
        	throw new ChartException(ErrorCode.MAX_CHART_COUNT_100);
        }
    }
}

 

[ 참고 및 출처 ]

부트캠프 수업 내용

https://reflectoring.io/bean-validation-with-spring-boot/

|  Enum이란?

- Enumeration Type : 열거체를 말한다.

- 핵심 : 사용자 지정 타입이며 실제 값은 0,1,2,3... 순번으로 출력된다.

- 부가 : 타입에 괄호()를 넣어 그 타입에 대해 설명할 수 있다.

 

|  Enum 문법

- 기본적으로 아래와 같이 열거하여 사용하는데

public enum AnimalType {
    CAT, DOG, HAMSTER, CHICKEN, SHEEP, LAMAR;
}

- 상수타입 옆에 괄호()를 넣어 상숫값을 명시할 수 있다.

public enum AnimalType {

    CAT("포유류","고양이"),
    DOG("포유류","개"),
    HAMSTER("포유류","햄스터"),
    CHICKEN("조류","닭"),
    SHEEP("포유류","양"),
    LAMAR("포유류","라마");

    private String species;
    private String kind;

    AnimalType(String species, String kind) {
        this.species = species;
        this.kind = kind;
    }

    public String getSpecies() {
        return species;
    }

    public String getKind() {
        return kind;
    }
}

- Main에서 테스트한 결과

import type.AnimalType;

import java.util.Optional;

public class Main2 {

    static class Patient {

        private Long id;
        private String patientNo;
        private AnimalType animalType;
        private String name;

        public Patient(Long id, String patientNo,
                       AnimalType animalType, String name) {
            this.id = id;
            this.patientNo = patientNo;
            this.animalType = animalType;
            this.name = name;
        }

        @Override
        public String toString() {
            return id + ".환자번호(" + patientNo + ") : "
                    + name + " (" + animalType.getSpecies()
                    + " " + animalType.getKind() + ")";
        }
    }

    public static void main(String[] args) {
        Patient patient = new Patient(1L, "1000101",
                AnimalType.CAT, "봄");

        Optional<Patient> patient2 = Optional.ofNullable(patient);
        System.out.println(patient2); // Optional[1.환자번호(1000101) : 봄 (포유류 고양이)]
    }
}

 

[ 참조 및 출처 ]

http://www.tcpschool.com/java/java_api_enum

'Language > Java' 카테고리의 다른 글

[이펙티브 자바] 객체의 파괴  (0) 2022.10.03
[이펙티브 자바] 객체의 생성  (0) 2022.09.29
JAVA 라이브러리 - Optional<T> 클래스  (0) 2022.09.15
SOLID 원칙  (0) 2022.08.29
JAVA 라이브러리 - 컬렉션  (0) 2022.08.11

|  Optional<T> 클래스란?

- 자바에서 모든 객체는 Referece Type으로 nullable하다. * null이 발생할 수 있는 타입

  > Referece Type의 객체는 null을 발생시킬 수 있다. -- NullPointerException

- Optional<T>은 객체를 nullable하게 쓸 수 있도록 하는 Wrapper class로, 객체 사용 시 null을 명시적으로 처리하게 한다.

  * 참고로 코틀린의 경우, nullable한 타입을 구분하는 코드가 별도로 존재한다.

Optional은 주로 "결과 없음"을 명확하게 나타내야 하고, 
null을 사용하면 오류가 발생할 수 있는 메소드 반환 유형으로 사용된다. 
Optional 변수는 null일 수 없으며 항상 Optional 인스턴스를 가리켜야한다. -- Oracle Docs

- 장점 : null을 직접 핸들링하지 않음, null 여부를 타입만으로 나타낼 수 있음, 체이닝을 통한 중간 및 종단 처리

  Optional<T> Optional.empty()
  T null
더보기

- Optional의 생성자를 보면, 아래와 같이 Objects.requireNonNull()을 통해

  감싸려는 객체 데이터가 null일 때, NullPointException을 발생시키도록 한다.

- 따라서 Optional로 감싼다고 하여도 NullPointException이 발생하지 않는 것은 아니다.

private Optional() {
    this.value = null;
}

private Optional(T value) {
    this.value = Objects.requireNonNull(value); // null이면 NullPointException 발생
}
public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

 

 

 

|  Optional 객체의 맴버 변수와 생성자

private static final Optional<?> EMPTY = new Optional<>();

private final T value;

private Optional() {
    this.value = null;
}

 

|  Optional 객체의 메소드

isPresent(): boolean null인지 아닌지 여부를 boolean으로 반환
of() 데이터를 Optional<T>로 감싼다.
ofNullable() of()와 동일하나 null이 발생할 때 처리 **null 발생 시 꼭 이 메소드 사용
get() null이 아닌 경우, 해당값 반환 **null일 때 예외 발생시키므로, 사용 시 주의요함
orElse(<T>) null일 경우, 특정 데이터를 반환
orElseGet(() -> getMethod()) null일 경우, 함수형 메소드 실행
orElseThrow(()->new Exception()) null일 경우, 지정한 예외 발생
filter() 조건식에 따라 필터링
map() 조건식에 따라 데이터를 매핑
flatMap() map과 동일하나 null처리가 다름

 

(1) of()와 ofNullable()의 차이

- ofNullable()의 경우 null이 발생할 때 empty() 메소드를 사용하는데, empty() 메소드를 쓰면 Empty 맴버 변수를 반환한다.

private static final Optional<?> EMPTY = new Optional<>();

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

(2) isPresent(), ifPresent() 

public boolean isPresent() {
    return value != null;
}

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

(3) get(), orElse(), orElseGet(), orElseThrow()

- get()은 사용을 지양하는 편이 좋다고 한다.* 감싼 데이터값이 null일 때, NoSuchElementException이 발생

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}

public T orElse(T other) {
    return value != null ? value : other;
}


public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

- orElse()과 orElseGet()의 차이점은 인자로 함수(ex. getRandomNumber())를 전달할 때 

  > orElse()의 경우 null이 발생하지 않아도 함수를 실행한 후 결과값을 orElse(//결과값//)에 전달하는 한 편,

  > orElseGet()을 쓰면 Supplier라고 하는 함수형 인터페이스를 통해

                                    null이 발생할 때에만 함수를 실행한뒤 결과값을 전달한다.

// 가정: optionalUser.findById(userId)는 null X

// getRandomUser() 실행 --> 결과값이 orElse()안에 담김
optionalUser.findById(userId).orElse(getRandomUser());

// getRandomUser() 실행 X
optionalUser.findById(userId).orElseGet(() -> getRandomUser());

- orElseThrow()는 null 발생 시, 지정한 Exception을 반환하게 하는 메소드이다.

(4) filter(), map(), flatMap()

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}

 

|  언제쓸까? - 값이 없을 수도 있을 때

(1) DB를 조회할 때

// JPA을 통해 Optional<T> 타입의 반환값이 null일 경우 처리하는 법
Optional<User> user = userRepository.findById(userId)
                                    .orElseThrow(() -> new Exception());

String userName = userRepository.findUserNameById(userId)
				.map(user -> user.getUserName())
				.orElse("이름 없음");

(2) 조회한 객체에 특정 칼럼이 비어있을 때

@Entity(name = "user")
public class User{
    @Id
    @GeneratedValue
    private Long id;
    
    private String userId;
    private String password;
    
    private String thumbnail;
    
    public Optional<String> getThumbnail(){
    	return Optional.ofNuallable(thumbnail);
    }
    
}

(3) 기존 시스템의 리턴 값이 null을 리턴할 때

Optional.ofNuallable(userRepository.findByName(userName))
		.ifPresent(user -> list.add(user));

(4) 필드에 Optional은 지양하기

- Optional은 함수 반환을 목적으로 만들어졌다.

- Serializable을 구현하지 않아 직렬화를 하면 이상한 값이 나온다.

[직렬화란]

메모리는 가상 메모리와 물리 메모리로 나뉘어져 있다.
가상 주소는 논리적인 주소일 뿐이지, 실제 저장되는 위치는 다를 수 있다.
객체를 그대로 저장하게 되면, 가상 주소까지 저장되게 되는데, 메모리 주소는 다른 시스템에 전달해도 의미가 없다.
따라서 불필요한 정보는 제외하고 타입 정보, 값 정보를 Byte형태로 만들어 보내는데 이를 직렬화라 한다.
직렬화를 하려면 클래스가 serializable를 구현해야한다.

- Optional.empty()를 빠뜨릴 확률이 높다. (휴먼 에러)

 

|  Optional을 제대로 쓰기

(1) 비어있을 컬렉션을 표현할 땐 Collections.emptyList()를 사용한다.

Optional<List<User>> findAll();  // !!사용하지 말아야 한다.

Collections.emptyList(); // Collections의 emptyList()를 사용한다.

- 컬렉션 요소에 Optional은 절대 금지

// 사용자가 Optional을 매번 판단해야하는 상황발생
List<Optional<User>> findAll();  // !!사용하지 말아야 한다. 

// ----사용시
for (Optional<User> opt : list) {

    // 매번 optional을 객체로 변환
    User user = opt.get(); // 불필요하다.

}

// User타입 그대로 반환
List<User> findAll();

(2) Optional을 파라미터로 넘기지 않는다.

(3) Optional 값을 가져올 때 .get()은 되도록 지양한다.

- get()을 쓰면, null일 때, NoSuchElementException을 발생시킨다.

- optional의 중단, 종단 메소드를 체이닝하여 사용하는게 낫다.

(4) Optional의 중간, 종단 메소드를 쓰기 위해 불필요한 옵셔널을 하지 않는다.

 

 

[ 참고 및 출처 ]

부트 캠프 수업을 들은 후 정리

https://tecoble.techcourse.co.kr/post/2021-06-20-optional-vs-null/

http://www.tcpschool.com/java/java_stream_optional

orElse()와 orElseGet()의 차이 : https://kdhyo98.tistory.com/40

'Language > Java' 카테고리의 다른 글

[이펙티브 자바] 객체의 생성  (0) 2022.09.29
JAVA 유용한 타입 - Enum  (0) 2022.09.15
SOLID 원칙  (0) 2022.08.29
JAVA 라이브러리 - 컬렉션  (0) 2022.08.11
JAVA 라이브러리 - 제네릭 클래스  (0) 2022.08.10

|  정리하게 된 계기

- 오늘 수업에서는 계좌 관련 프로그램을 만드는 걸 시작했고,

  계좌 생성 API를 만들며, 사용자 및 계좌에 대한 Entity를 만들었었다.

- 만들다보니, @Entity, @EntityListeners, @id, @Generated의 상위 패키지persistence로 되어 있는게 보였다.

- 더불어, 객체 이벤트 발생 시간을 알려주는 @EntityListeners(AuditingEntityListener.class) 코드의

  AuditingEntityListener.class로 가, 설명을 읽어보니,

  "이건 Entity에 persist 또는 update 이벤트가 발생하는 시간을 알려주는 일회성(stateless) 클래스이다"

  라고 적혀있어, persist가 뭐지? 하는 생각을 하게 됐다.

 

|  JPA에서 말하는 영속성이란

- JPA는 풀어서 Java Persistence API라고 했다.

- 앞서서는 위의 persistence란 자바 코드를 유지하는 것을 말한다고 했는데,

  사실 * persist()란, "영구적으로 DB에 반영하다"를 의미한다.

  그러나 JPA는 정확히 자바를 통해 DB에 영구적으로 반영하는 API라는 의미였다.

- 우리가 JPA를 통해 어떤 테이블, 다른 말로 Entity를 만들고자 하면, 아래와 같은 단계를 거칠 수 있다.

  비영속(new) 영속(managed) 준영속(detached) 삭제(removed)
메소드 new persist() detach() - 특정 entity 분리 remove()
특징 자바 Object 생성 DB에 반영 엔터티 매니저 관리X 삭제
persistence
context에
Entity포함
X O X X

- 영속성 컨텍스트(persistence context)는 실제하진 않고 논리적인 개념으로, 엔터티를 영구 저장하는 환경을 말한다.

- 우리는 자바를 통해 객체를 만들고 이를 DB에 반영할 것을 JPA api에게 요청한다. 

  요청이 DB에 반영이 되는 순간이 곧, persist()되는 순간이다.

출처:https://victorydntmd.tistory.com/207

- 그러면, 다시 Entity를 만드는 자바 코드로 돌아가서 생각을 해보겠다.

package com.example.animal.domain;

import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@EntityListener(AuditingEntityListener.class)
public class Patient{	
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String kind;
    private String sex;
   
    @OneToMany
    @JoinColumn(name = "PatientOwner_id")
    private PatientOwner patientOwner;
    
    @CreatedDate
    private LocateDate createdAt;
    @LastModifiedDate
    private LocateDate updatedAt;
}

- 앞에서 만들었던 동물병원 진료 예약 시스템에서, 동물을 Patient로 해서 만들어봤다. 

  간단한 내역만 적어두었는데, Entity에 관한 Annotation import를 보면 persistence가 패키지명으로 들어간 게 보인다.

- 다시 말해 @Entity 표시만 해두고, @Id나 @GeneratedValue로 pk를 설정하면 알아서 DB에 영구 반영시키겠단 말이다.

- 그리고 @EntityListener는 이 과정에서 발생하는 이벤트 상황을 원하는 바에 따라 읽어오겠다는 이야기이다. 

- 그런데 잠시 고민해야 하는 부분이 더 있다.

- @EntityListener를 저렇게 표시만 해둔다고 쓸 수는 없단다.

   Listener는 말 그대로 이벤트가 발생할 때까지 귀기울여 대기하는 녀석이기에,

   대기할 수 있게 미리 DI Container에 넣어주어야 한다.

 

|  AuditingEntityConfig 클래스를 만들기

- 우리는 EntityListener중에서도 AuditingEntityListener를 쓰려고 한다.

- 그리고 앞서서 스프링은 처음에 프로그램을 시작할 때, DI container안에 의존성을 주입한다고 했으며,

- 리스너는 미리 이 container안에 넣어서 대기시켜주어야한다고 했다.

* DI container에 의존성을 주입하려면 @Configuration을 붙인 설정 클래스 안에서 @Bean을 불려오는 것으로 할 수 있는데

   이 밖에도 @Component 등이 @Bean을 상속하여 붙이는 순간 시작 시 자동으로 의존성이 주입되기도 했다.

- 리스너를 대기시키기 위해 Config 클래스를 하나 만들고 @Configuration과 @EnableJpaAuditing을 붙인다.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {

}

 

|  정리

- JPA에서 말하는 영속성이란, DB에 영구적으로 반영되었음을 의미한다.

- @Entity, @EntityListener, @Id, @GeneratedValue 에는 이러한 영속성의 의미가 패키지명에 담겨 있다.

- @EntityListener(AuditingEntityListener.class)는 Entity에 create/update 이벤트가 발생하는 시간을 알려준다.
  - Entity 내부의 맴버에 @CreatedDate와 @LastModifiedDate를 추가하고,
  - JpaAuditingConfig 클래스를 만든 후 @Configuration과 @EnableJpaAuditing을 추가하여 사용한다.

 

 

 

[ 참조 및 출처 ]

부트캠프 수업 후 정리

JPA persistence context https://victorydntmd.tistory.com/207

Entity Listeners https://wave1994.tistory.com/161

1.  프로젝트 생성 및 요구사항 분석

- 오늘 수업시간에서는 프로젝트 생성 및 요구사항 분석 + 설계 및 기본 구조 개발에 대해서 들었다.

- 각 회사마다 분위기는 다 다르겠지만, 요구사항 분석 시엔 일반적으로 아래의 단계를 거친다고 한다.

  1) 외부 고객 및 내부 고객의 의견에 따라 어떤 사업을 하겠다는 목표가 생기면,

  2) 기획자가 기획안을 만들어 연관됨 팀들과 의견을 나눈 뒤 사용할 기술 및 주요 기능에 대해 보완한다.

  3) 기획자의 기획안을 토대로 개발팀은 개발을 위한 기술 스택구체적인 명세서를 작성한다.

 

(1) 기술 스택이란 뭔가

- 위 3) 단계에서 "기술 스택"이라함은,

- 어떤 프레임워크와 언어를 쓸 것인가

- 어떤 DB vendor를 쓰고

- 어떤 api를 사용할 것이며

- http 데이터 타입은 어떤 걸 쓸 건지 (ex. xml인지, json인지, yml인지 등)

개발을 위한 기본적인 구조를 정하는 걸 말한다.

- 일전에 회사를 다닐 때 적은 설계서를 떠올리면, 주로 기획안 초반에 작성되는 항목들 중 하나였던 걸로 기억한다.

- 기획안에서 간단히 다루어주긴 하겠지만, 내부적으로도 체크가 필요할 것이라 생각된다. 

 

(2)  구체적인 명세서란

- 기획안을 받으면 개발팀 내부에서도 본격적인 개발에 돌입하기 위해 구체적인 명세서를 작성하기도 한다고 한다.

- 여기서의 구체적인 명세서란, 각 Entity에 대해 이루어지는 작업을 API로 하여 정리한 걸 말하는데

- 검색을 해보니 이건 개발자의 성향에 따라 조금씩 다 다른 것 같았다. (아예 안 쓰는 사람들도 있고)

- 수업에서는 Entity 별로 Http 요청에 대한 처리 API들로 간략히 정리했었다.

- 연습을 한 번 해보고자 동물병원 진료 예약 시스템을 생각해서 하나의 API만 작성해보았다.

ex. 동물병원의 진료 예약 시스템

사용자
  // 생략

동물
  - 동물 생성 API
     - POST /animal-register
     - 파라미터 : 사용자 아이디
     - 정책 : 사용자가 없는 경우 실패 응답
     - 성공 응답 : 사용자 아이디, 동물번호, 등록일시
   .... 등등등

예약정보
  // 생략

 

2.  기본 구조는 어떻게 잡을 수 있을까

- 기획안에서, 또는 기획안을 받은 후에, 기술 스택에 대해 정하게 되는데, 구체적으로는, 

  코딩 컨벤션이나, 패키지 구조, DB 접속 환경, 연동 API등에 대해 전반적인 틀을 잡는다.

  (이걸 수업시간에선 기본구조를 잡는다고 설명했다.)

 

- 코딩 컨벤션은 팀 내부에서 사용하는 코딩 스타일을 말한다.

  자주 사용하는 코딩 컨벤션 도구로 ESLint와 Prettier가 있다고 하는데, 자세한 건 바로가기 에서 보면 좋을 것 같다.

 

- 패키지 구조는 앞서서 이야기했듯 디자인패턴에 따라 달라질 수 있다. 

  (예를들어, 레이어패턴인 경우, N-tier 방식으로 패키지를 만들고, 헥사고날의 경우, 도메인 중심으로 패키지를 만든다.)

  따라서, 회사에서 요구하는 디자인 패턴에 따라 그 패턴을 공부하고 적용하는 게 필요할 것 같다.

 

3. 명세서에 따라 Entity의 상세정보를 정리하고 개발 시작하기

- 수업에서는 명세서에서 작성한 API 하나에 대해,

- 1) 이미 검토한 정보(HTTP 메소드, 파라미터, 정책, 성공 응답) 와 더불어

  2) 상세 정보 (Entity내에 추가되어야 하는 정보)를 정리 (수업에선, 컬럼명과 데이터타입과 설명까지 정리했다.)

  3) 요청/응답 시 주고받는 데이터 구조까지 정리했다.

- 상당히 도움이 되는 흐름이었는데, 사용자 케이스에 맞춰 요청 API를 먼저 처리하고,

  관련된 객체(Entity)를 만든 후에, 단위 테스트를 하게 되면 개발자도 흐름을 체크하며 개발할 수 있지 않을까 싶다.

 

 

[ 참고 및 출처 ]

부트캠프 수업을 들은 후 정리한 내용입니다.

https://mklab-co.medium.com/%EC%9E%91%EC%84%B1%EB%B2%95-%ED%99%94%EB%A9%B4%EC%84%A4%EA%B3%84%EC%84%9C-wireframe-%EC%99%80-%EA%B8%B0%EB%8A%A5%EB%AA%85%EC%84%B8%EC%84%9C-functional-specification-bbcff0071ea2

|  개요

- 수업 시간에 고객의 계좌 정보에 관한 프로그램을 샘플로 만들며 아래의 내용을 배우기로 했다.

- 저작권상 내용을 여기에 담기는 좀 그래서 전반적인 개념에 추가적인 정보를 담아 스스로의 이해를 위해 정리해보려고 한다.

- 강사님의 강의 흐름이 마치 현장 플젝을 해결하는 것 같이 느껴지는데, 이 흐름만 잘 기억해도, 차후 개인 프로젝트를 기획할 때 많은 도움이 될 것 같다.

 

no 흐름 특징
0 개요 시스템 한 줄 소개 / 활용 기술 요약 / 프로젝트 엔터티 구조 안내 / 주요 제공 기능(API) - 분류
1 프로젝트 생성 및
의존성 추가
spring.io 또는 인텔리J를 통해 스프링 프로젝트 생성, 의존성 추가
* 어떤 기술을 쓸 건지에 따라 의존성은 달라질 수 있다.
2 Lombok Lombok을 통한 dto 생성
3 HTTP 프로토콜  HTTP 요청에 따라 Client의 요청사항 분석하고 응답하기
4 H2 DB 개발을 할 때 또는 테스트 시에 H2 DB를 통해 개발한다. 
* RDB와의 소통은 JPA를 사용하도록 한다.
5 트랜잭션 DB의 상태를 변화시키는 작업단위. ACID 
6 Embeded Redis 싱글 쓰레드 기반의 일종의 noSql이며 메모리DB를 사용한다.
Spinlock으로 동시성을 제어하여 빠르게 휘발적으로 데이터를 사용할 때 Redis를 사용한다. 
7 테스트 테스트를 왜 할까? TDD와 단위적 테스트의 차이점은?

 

1. 의존성 추가하기 

- 인텔리j에서 보면, 프로젝트 바로 하위에 "build.gradle"이라는 파일이 있다.

- 앞에서도 잠깐 언급했는데, dependencies 안에 의존성을 추가할 수 있다.

  인텔리j에서 플젝 시작할 때 Lombok이나 JDBC 같은 라이브러리를 추가하면 자동으로 의존성이 추가되는데,

  별도로 직접 작성해줄 수도 있다.

/* 플러그인의 의존성(library) 관리 */
plugins {
	id 'org.springframework.boot' version '2.7.3'
	id 'io.spring.dependency-management' version '1.0.13.RELEASE'
	id 'java'
}

group = 'com.zerobase'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

/* 각종 의존성들(libraries)을 어떤 원격 저장소에서 받을 것인지 지정 */
repositories {
	mavenCentral() // jcenter로 업로드 설정을 간소화할 수 있다.
}

/* 프로젝트 개발에 필요한 의존성들을 선언하는 곳 */
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

2. Lombok

- Lombok은 DTO(=VO) 객체의 Getter/Setter, 생성자를 자동으로 생성해주거나,

  특정 클래스를 지정해서 로그를 자동으로 생성해주는 라이브러리이다.

- 다양한 기능

구분 표기 내용
게터/세터 외 다수 @Data 게터/세터/toString/Equals/HashCode 등 한번에 제공

** @Data의 경우, 사용자의 정보가 toString으로 그대로 노출되는 등의 이슈가 발생할 수 있으므로 사용에 주의하는 것이 좋다.
게터/세터 @Getter/@Setter 게터/세터 자동 생성
생성자 @NoArgsConstructor 기본 생성자
@AllArgsConstructor 모든 필드 포함 생성자
@RequiredArgsConstructor 지정 필드 포함 생성자 *맴버 중 private final로 된 맴버 필수 포함
로그 @Slf34j 이 클래스의 로그 자동 생성
toString @ToString Object의 toString
빌더 패턴 관련 @Builder 빌더 패턴 자동 생성 (생성자의 인자가 너무 많을 때, Builder를 쓴다.)

ex. SomeClass.builder().name().weight().height().score().build()
생성자 private화 @UtilityClass static 메소드만 제공하는 유틸리티 클래스의 생성자를 private화

- Delombok을 통해 Lombok으로 만들어진 생성자 및 메소드 확인하기

  : DTO 객체에서 우측 마우스 - [Delombok]을 누르면 생성자 및 메소드가 자동 생성된다.

- 참고로 @Autowired를 사용해도 생성자나 게터, 필드를 만들 때 의존 객체의 타입을 자동으로 주입해주는데, 

  생성자의 인자가 많이 있거나 수정할 때마다 매번 타이핑을 해야하기 때문에 현재는 사용을 권장하지 않는다. 

  @Autowired 대신 롬복을 사용하는 것이 좋다.

 

3. HTTP (Hyper Text Tranfer Protocol)

- 하이퍼텍스트란, 링크를 연결시켜주는 텍스트를 말한다. (마치 이것처럼)

- 과거에는 단순 텍스트 링크(하이퍼텍스트)를 통해서 데이터를 주고 받았었다.
- 하이퍼텍스트가 발전하여, HTML, 그리고 JSON 등이 생겨나게 되었다.
- 이러한 데이터 전송 방식은 Hyper Text에서 사용하던 프로토콜, 곧 HTTP을 따른다.

- HTTP 프로토콜에 대한 요약 정리

HTTP2.0_출처:https://velog.io/@taesunny/HTTP2HTTP-2.0-%EC%A0%95%EB%A6%AC

- 한 컴퓨터에서 다른 컴퓨터로 데이터를 요청할 때에는 데이터를 패킷이라는 단위로 묶어 주는 과정을 거친다.
  * OSI 7 Layer : Application -> Presentation -> Session -> Transport -> Network -> Data Link -> Physical
  * TCI/IP          : Application                                             -> Transport -> Internet -> Network Interface
  WWW는 TCP/IP방식을 따르므로, 그를 기준으로 보면 HTTP는 Application 단계(최상위 단계)에 해당된다.

- Application단계에서 Client는 URI로 요청사항을 날리고, 그 요청사항은 HTTP의 헤더와 바디에 담겨진 뒤, 나머지 하위 과정에서 포장된다. 포장된 데이터는 여러 네트워크망을 거쳐 Server로 넘어가게 된다.

- HTTP는 POST/GET/PUT(PATCH)/DELETE 라는 요청방식을 가진다.

- HTTP는 본래 Connectless, 한 번 연결하면 끝나는 일회성 연결 방식을 가졌으나,
  HTTP 1.1로 버전이 올라가면서 keep-alive를 통해 일정 기간 동안 연결 상태를 유지하게 되었다.
  * TCP/IP 방식에 따르면, 매번 연결할 때마다 3-way-handshake가 필요하다. 1.1에서는 일정 기간 이걸 하지 않는다.
  요즘에는 또 HTTP 2.0 버전이 나왔는데, 가장 큰 특징은 1.1은 한 번에 하나의 파일만 전송 가능했다면, 2.0은 이제 여러개 파일을 병렬처리로 전송하는 Multiplexing이 된다는 것. (그러나 헤더와 바디의 큰 구조는 위 그림처럼 비슷하다)

- 추가적으로,
  HTTP는 본래 Stateless, 통신이 끝나면 상태를 유지하지 않았지만  쿠키와 세션으로 상태를 유지할 수 있다.
  특정 정보(ex.로그인 정보)를 쿠키는 브라우저에, 세션은 서버의 id를 이용해 암호화해 서버에 저장 후 브라우저에도 저장한다.
  * 단, 연결 상태 유지 기간을 설정하면, 데이터는 브라우저가 아닌 로컬에 저장된다. 

- 정리하면, 오늘날의 HTTP는 URI를 통해 요청을 받는 것은 동일하되,
                  일정 기간 동안 연결을 유지할 수 있고,
                  데이터 송수신 시 병렬 처리가 가능해졌으며,
                  쿠키와 세션을 통해 특정 상태(ex. 로그인 상태)를 브라우저(또는 로컬)에 저장하여 유지시킬 수 있다.

(1) HTTP Request

POST /path HTTP/1.1                   // Header 첫줄 : Method 경로 HTTP버전
Content-Type: application/json        // 받은 콘텐츠 타입
Accept: application/json              // 보낼 콘텐츠 타입 (응답)
UserInfo: {"userId","test1234"}       // 인증키나 회원명 등을 넣는다

{                                     // Body
    "phone" : "010-1234-5678"
}

(2) HTTP Response

HTTP/1.1 200 OK                      // Header 첫줄 : HTTP버전 상태정보 메세지
Content-type: application/json       // content-type부터 연결(keep-alive)유지 기간 등
Transfer-Encoding: chuncked          
Date: Mon, 21 Jun 2022 12:11:23 GMT  
Keep-Alive: timeout=60
Connection: keep-alive

{                                   // Body : 응답 받는 Clinet에게 전달할 데이터
   "message" : "등록 성공"
}

 

4. H2 DB를 사용하기

(1) H2 DB의 특징 

: 메모리/파일 관계형 DB로, 가볍고 시작할 때마다 자동 삭제-생성 가능하며, 대다수의 DB sql명령어와 호환되어,

: 개발을 하면서 테스트를 하기에 좋은 DB이다.

  • 빠르고 오픈소스인 JDBC API
  • In Memory DB(인 메모리 DB)*
  • Embedded mode(내장모드) & Server mode(서버모드) 지원
  • 브라우저 기반 콘솔 프로그램
  • 2MB정도의 적은 용량으로 설치 가능
  • ANSI 표준 SQL로 여러 호환성 모드 지원
[출처] https://jamie95.tistory.com/188

(2) H2 DB를 설정 - application.yml 로 설정하기

: 스프링 플젝을 처음 생성하면, [src] - [java] - [main] - [resources] 안에 "application.properties" 파일이 있다.

: 애플리케이션에 들어가는 설정들을 정리해서 넣는 파일인데, 이 파일의 확장자를 .properties에서 .yml로 바꾼다.

  rf. .yml파일은 야믈이라고도 하는 yaml 데이터 타입을 말한다. (+ 자세한 내용은 하단의 더보기에)

: 아래의 내용을 보면, 스프링 설정을 야믈 데이터 타입으로 안내하는 것인데 크게 보면,

  [ DB에 대한 설정(위치와 JDBC정보) + H2 콘솔 사용 여부 + JPA 관련 설정(INSERT시작 지점, DB정보, 쿼리 관련 설정) ]

  이렇게 세가지를 아래에서 하고 있는 걸 볼 수 있다.

spring:                                        // spring 플젝의 설정할거다!
  datasource:                                  // (1) DB의 소스 : 
    url: jdbc:h2:mem:test                      // 5가지 정보(url-ip와port와instance),계정,비번)
    username:                                  // -- ip와 port대신 mem (memory를 의미)
    password:
    driverClassName: org.h2.Driver             // JDBC 드라이버 정보
  h2:                                          // (2) h2 console 사용 여부 
    console:
      enabled: true
  jpa:                                         // (3) jpa 관련 설정
    defer-datasource-initialization: true      // table 생성 후로 resource의 data.sql 입력 미룬다(defer)
    database-platform: H2                      // db vendor 
    hibernate:                                 // hibernate (jpa 인터페이스 구현체)
      ddl-auto: create-drop                       // ddl-auto : 자동으로 실행시 drop, create 
    open-in-view: false                        // open-view (뷰에서 볼 것인지)
    properties:                                // properties (설정)
      hibernate:                                 // hibernate - sql 형식으로 쓰고 볼지
        format_sql: true
        show_sql: true
[ JPA란 ? ]

- 출처 : https://dbjh.tistory.com/77?category=853400 
- JPA란, Java Persistence API의 약자로, 말 그대로 RDB와 소통할 때도 자바를 지속적으로 쓰게 해주는 API이다.
- 조금 어려운 말로는, 자바에서 ORM(Object-relational Mapping) 기술 표준으로 사용되는 인터페이스 모음이라고 하는데,
- 결국 ORM도 객체와 RDB를 연결(매핑)해주는 걸 의미한다.
- JPA는 JPA라는 인터페이스가 있고, 그를 hibernate, OpenJPA등이 구현한다.

- 장점 :
  - SQL문이 아닌 자바언어로 DB를 조작하기 때문에, 비즈니스 로직에 맞춰 개발하기 편리하며 가독성이 높다.
  - 객체 지향적인 코딩이 가능해 진다. (상속 또한 RDB로 자동 표현할 수 있게 했다.)
  - DB vendor를 변경해도 코드를 수정하지 않아도 되어 vendor에 대한 의존도를 낮추고 리팩토링에 유리하다.

- 단점 :
  - 프로젝트 규모가 크고 복잡한 상태일 때, 설계가 잘못 될 경우, 속도 저하 및 일관성이 떨어질 수 있다.
  - 복잡하고 무거운 쿼리문의 경우 속도를 위해 별도로 SQL문을 써야할 수 있다. 
  - JPA를 배우는 비용이 비싸다.

- 설정을 다 하고, 어플리케이션을 이클립스나 인텔리j에서 실행하면, 에디터 콘솔에 Tomcat을 통해 알아서 잘 생성됐다는 메세지가 뜬다.

- 이제, 브라우저에서 아래의 정보를 입력하면 H2 DB 콘솔을 볼 수 있다.

http://localhost:8080/h2-console

- 설정 야믈에 써놓은 URL과 계정명, password를 잘 써주고 연결하면 된다. (JDBC URL이, url이다)

* 처음에 JDBC URL은 jdbc:h2:~/test;로 되어 있는데, 이 부분을 아까 야믈 설정파일에서 설정해두지 않았다면,

  매번 실행할 때마다 주소가 자동 할당되어 에디터 콘솔에 뜨고, 그걸 복사해서 위에 붙여주어야만 한다.

더보기

[ 이름도 귀여운 야믈은 뭘까? ]

 

- 데이터를 전송할 때의 규칙, 약속을 우리는 데이터 타입이라고 한다.

- 데이터 타입에는 XML, JSON 등이 있는데

  - XML은 <>과 들여쓰기로 각 데이터간 관계성을 나타내고

  - JSON은 key와 value로 데이터간의 관계성을 나타낸다면

  - YAML은 - 하이픈을 통해 깔끔하게 데이터간의 관계성을 표현한다.

출처:https://www.inflearn.com/questions/16184

- 야믈이 귀여운 이름만큼 우리 눈과 마음을 참 깔끔하게 해주는 녀석이라고 기억해두면 좋을 것 같다.

(3) 레이어 디자인 패턴으로 패키징을 하고 JPA를 통해 domain 객체를 만든다.

- 앞서서, 스프링은 MVC 3-tier 아키텍처를 따른다고 했다.

* MVC 3-tier 아키텍처는 SOLID의 SRP원칙(단일책임원칙)을 지키도록 해주는데, 

  [1] 표현층 (Presentation Layer)

  [2] 비즈니스 로직층(Business Logic Layer)

  [3] 데이터 엑세스층(Data Access Layer)

  로 나누어져 있으며, 표현층의 컨트롤러로 비즈니스 로직층으로 넘어간 뒤 DB와 상호작용(Service)하여 처리한다.

- Layered는 n-tier 방식으로 코드를 짜는 디자인 패턴을 말하는 것으로, 각 의존 관계가 마치 레이어처럼 겹겹이 쌓여있다.

  * 실습에서는 controller, service, domain(dto), repository로 나누었는데 아래와 같이 나누어볼 수 있었다.

3-tier 패턴의 레이어 패키지명 특징 의존 Annotation
Presentation Layer controller http 메소드로 관련 service 호출 service @RestController
@GetMapping 등
domain model : dto(vo),
dto에 들어가는 타입(enum)
- @Entity
@Enumerated
Service Layer service repository의 메소드를 구현하여 db에 쿼리 전달 repository @Service
@Transactional
Repository Layer repository jpa를 통해 db의 table과 직접적으로 연동 - (db와 소통) @Repository

- 현장에서는 다만, 이와 같은 디자인 패턴 보다는 Hexagonal Architecture 디자인 패턴을 더 선호한다고 한다.

 

(3-1) 테이블 모델

: 레이어 패턴에서는 dto를 domain안에 만들었는데, @Entity를 통해 이 클래스가 테이블임을 표현한다.

: 이렇게 생성한 후에 다시 애플리케이션을 실행하면, H2 console 부분에 테이블이 생성된 게 보인다.

// import 생략

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder                // 여기까지 Lombok을 사용해 builder, 게터/세터를 만들게 한다.
@Entity                 // Entity를 쓰면 이 클래스를 Entity로 설정하겠다는 의미이다.
public class TableName{

    @Id                 // pk인덱스 생성
    @GeneratedValue     // pk가 Sequence인지, Table인지 등과 세부 제한 설정
    private Long id; 
    
    private String certainMember1;
    
    private String certainMember2;
    
}

 

(3-2) 리포지토리 만들기

- JPA를 통해 DB와 직접적인 소통을 하는 것은 repository interface이다.

- 중요한 건, 이것은 인터페이스이며 클래스로 만들면 안 된다는 것이다. 

  * 왜 인터페이스로 되어 있을까? 그건 DIP 법칙에 따라 JPA에 너무 의존하지 않게 하려는 것 아닐까? (좀더 생각해보자..)

- JpaRepository<테이블객체명,pk타입>를 구현한 인터페이스를 만든다.

@Repository
public interface CertianRepository extends JpaRepository<TableName, Long> {

}

- JpaRepository의 메소드들은 굉장히 많았는데 그 중에 기본적인 메소드들은 아래와 같았다.

save(S entity) : S
saveAll(Iterable<S> entities) :  Iterable saveAll
저장하기
findById(ID id) : Optional<T>
findAll() : Iterable
조회하기
deleteById(ID id) : void 
delete(T entity) : void 
deleteAllById(Iterable<? extends ID> ids) : void 
삭제하기
existsById(ID id) : boolean 
존재여부
count() : long  갯수반환

 

(3-3) 서비스 만들기

- 서비스는 리포지토리 인터페이스를 구현한다.

- 바로 위의 표의 메소드들 (save, findById, delete, count 등)을 호출하는 클래스를 만든다.

@Service
@RequiredArgsConstructor
public class CertainService {
    private final CertainRepository certainRepository; 

    @Transactional
    public void createOneRow() {
        TableName tableName = TableName.builder()
                .memberfield("hahaha")
                .build();
        certainRepository.save(tableName);
    }

    @Transactional
    public TableName getOneRow(Long id) {
        return certainRepository.findById(id).get();
    }
}

 

(3-4) 컨트롤러로 URI에 대한 HTTP 처리하기

- 앞서서 Client는 브라우저의 URI를 통해 요청을 보낸다고 했다.

- 이제 어느 경로에서 어떻게 URI를 보내면, 특정 서비스의 메소드를 호출할지를 여기서 정해주면 된다.

@RestController
@RequiredArgsConstructor
public class CertainController {
    private final CertainService certainService;

    @GetMapping("/create-certain")
    public String createCertain() {
        certainService.createOneRow();
        return "success";
    }

    @GetMapping("/certain/{id}")
    public Account getAccount(@PathVariable Long id) {
        return certainService.getOneRow(id);
    }
}

 

5. 트랜잭션

트랜잭션 : 데이터베이스의 상태를 변경시키기 위해 수행하는 작업단위

[출처] 코딩팩토리, https://wonit.tistory.com/462

- 트랜잭션은 DB의 상태를 변경시키는 것으로, CRUD(INSERT, SELECT, UPDATE, DELETE) 행위를 말한다.

(1) Commit과 Rollback

- 트랜잭션은 순차적으로 Commit -저장-을 하며 실패해도 로그를 남겨, 앞의 트랜잭션이 다 끝나야 실제로 반영한다.

- 만약에, 트랜잭션이 비정상적으로 종료한다면 Rollback -철회-을 통해, 트랜잭션 전체 또는 부분적으로 결과를 취소한다.

(2) 트랜잭션의 상태

- 트랜잭션은 실행중(Active)이거나

  - 커밋 일부 완료(Partially Commited), 커밋 모두 완료(Commited)

  - 실행중 오류 발생으로 실패(Failed), 비정상적 종료로 rollback 수행(Aborted)

- 하는 총 5가지의 상태를 가질 수 있다.

(3) 트랜잭션의 특징

- 트랜잭션에는 4가지의 특징이 존재하는데,

ACID 내용
Atomic (원자성) All or Nothing : 모든 작업이 실행되거나 or 모두 실행되지 않아야 한다.

rf. 결제서비스에 관한 트랜잭션 중 갑자기 오류발생으로 끊긴다면? 
    ㄴ 결제서비스에 관한 트랜잭션 전체는 실행되지 않게 된다. 
Consistency (일관성) 트랜잭션 작업 결과는 항상 일관적이어야 한다.

* 모든 트랜잭션이 종료된 후엔 모든 DB의 제약조건을 지키고 있는 상태여야 한다.

rf. 웹툰 결제를 할 때 최소 결제 단위는 100원이어야 한다면, 
    결제 서비스 트랜잭션이 실행될 때 내 계정엔 100원 이상이 남아있어야 한다.
Isolation (격리성) 트랜잭션은 다른 트랜잭션과 독립적이어야 한다.

rf. 현실적으로 트랜잭션의 격리성을 지키기가 어렵다 (성능과 안정성의 trade-off)
- 격리성의 단계 ) 
  READ_UNCOMMITED > READ_COMMITED > REPEATABLE_READ > SERIALIZABLE
- 대체로 현장에서는 REPEATABLE_READ 를 한다고 한다.
Durability (지속성) 트랜잭션이 완료되면 영구적으로 결과에 반영되어야 한다.

* 트랜잭션은 순차적으로 commit되며, commit이 실패해도 모든 로그를 남겨 db에 반영된다.

 

6. Embedded Redis 실행

레디스는 고성능 In-Memory 키-값 저장소로서 
문자열, 리스트, 해시, 셋, 정렬된 셋 형식의 데이터를 지원하는 NoSQL이다.

[출처] https://devlog-wjdrbs96.tistory.com/374

- 위의 말을 풀어서 이해했을 때, 레디스는 결국 DB의 데이터를 휘발적으로 메모리에 직접 넣어서 빠르게 처리할 수 있게 해주는 녀석이며, 데이터의 모양은 noSql 중에서도 키-값 형태로 저장되는 녀석인 것 같다.

+ noSql에 대해 더 알고 싶다면 아래의 더보기로

- Redis는 SpinLock을 통해 동시성 제어를 해주며, AOP를 실습할 때에 따라서 자주 사용된다고 한다.

- 나의 경우에는 수업에 따라 인텔리j를 쓰고 있어서 별도로 Redis를 설치하는 곤욕을 겪지 않았는데, 따로 설치해주는 경우도 있는 모양이다. 그게 필요한 경우에 잘 나와 있는 페이지가 있어서 갖고 왔다. 

https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-Window10-%ED%99%98%EA%B2%BD%EC%97%90-Redis-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0

 

[REDIS] 📚 Window10 환경에 Redis 설치 & 설정

Redis 윈도우 설치 Redis 다운로드 페이지로 이동하여 설치 프로그램을 다운로드하고 설치를 진행한다. Releases · microsoftarchive/redis Redis is an in-memory database that persists on disk. The data mo..

inpa.tistory.com

(1) Redis를 dependencies에 추가 & application.yml에도 host/port를 설정해준다.

- Redis도 라이브러리이기 때문에 쓰려면 의존성을 추가해주어야 한다.

- Build.gradle에 들어가서, 아래와 같이 dependencies를 작성해 주었다.

- 아래에서 exclude된 부분은 이미 롬복에서 처리해주는 부분이기 때문에, 제외하지 않으면 오류가 난다고.

  그래서 이렇게 제외를 해주는 게 좋다고 한다..

dependencies {
	// redis client
	implementation 'org.redisson:redisson:3.17.1'
	// embedded redis
	implementation('it.ozimov:embedded-redis:0.7.3') {
		exclude group: "org.slf4j", module: "slf4j-simple"
	}
}

- Redis 또한 in-memory 방식의 noSql이기 때문에 다른 DB들과 마찬가지로 host와 port가 필요하다.

- 앞에서 만들어주었던 application. 야믈파일에서 아래와 같이 호스트와 계정을 넣어준다.

  * 보통 Redis는 6379 포트를 쓴다고 한다.

spring:
  redis:
    host: 127.0.0.1
    port: 6379

(2) LocalRedis 실행 설정하기

- Config 클래스를 하나 만들어, 포트정보를 통해 RedisServer를 만들어준다.

- RedisServer는 @PostConstruct@PreDestroy를 할 수 있는 메소드를 생성해주는데,

  Bean의 생성주기에서 빈 생성 및 의존관계 주입 후 @PostConstruct가 이루어지고, 종료 전에 @PreDestroy가 이루어진다.

 

[  Bean의 생성주기 ]

스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입
초기화 콜백(EVENT) → 앱 본연의 동작 수행 → 소멸전 콜백(EVENT) → 스프링 종료
@Configuration
public class LocalRedisConfig {
    @Value("${spring.redis.port}")
    private int redisPort;
    private RedisServer redisServer;

    @PostConstruct
    public void startRedis() {
        redisServer = new RedisServer(redisPort);
        redisServer.start();
    }

    @PreDestroy
    public void stopRedis() {
        if (redisServer != null) {
            redisServer.stop();
        }
    }
}

(3) LocalRepository 등록

- Config 클래스를 하나 만든 후에, 호스트와 포트 맴버 필드를 만들고, RedisClient를 호출하는 메소드를 만든다. 

- 이 설정 자체도 너무 어려웠다..  암기가 필요한 순간.

@Configuration
public class RedisRepositoryConfig {
    @Value("${spring.redis.host}")    // EL같다. application의 데이터를 가져온다.
    private String redisHost;

    @Value("${spring.redis.port}")    // EL같다. application의 데이터를 가져온다.
    public int redisPort;

    @Bean
    public RedissonClient redissonClient() {  // RedissonClient을 반환해주어야한다.
        Config config = new Config();  // Config 객체 생성
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);

        return Redisson.create(config);
    }
}

 

7. 테스트

- 과거의 경우 어떤 서비스가 만들어지면 그것을 하나하나 검수하는 과정을 거쳤었다.

- 현재에는 자동화된 테스트 코드를 통해 빠르게 테스트를 하고 있다. 

- 다양한 테스트 커버리지와 테스트 방법론이 존재한다.

  과거의 테스트 현재의 테스트
방법 기능적으로 하나하나 일일히 검수 자동화된 테스트 코드
특징 다수의 인력, 시간이 너무 많이 든다 빠른 속도로 테스트 가능, 코드 수정 후 재검수 용이

[ 테스트에 관한 개념 ]

(1) 테스트 커버리지 : 단위 테스트 / 전 구간 테스트 / 통합 테스트

  단위 테스트(Unit Test) 전 구간 테스트(End-to-End Test) 통합 테스트(Integration Test)
크기 클래스 또는 메소드  현재부터 ~ 배포까지 연관된 모든 기능 전반(외부lib까지)
장점 - 개발자 관점의 빠른 테스트
- TDD와 함께 할 때 강력하다.
- 내부 기능까지 테스트하지 않는다
- 사용자 관점에서 E2E(End to End) 형식을 이용해 확인
- 개발자가 변경불가한 부분 확인
- 단위 테스트에서 발견이 어려운 버그까지 커버
단점 - 단위를 벗어난 영역의 버그를 확인하기 어려움 - 테스트를 만들기가 힘들다
- 많은 코드를 테스트해 신뢰성 ↓
- 많은 코드를 테스트해 신뢰성 ↓
- 에러 발생 지점을 찾기 어렵다

- 유지보수가 힘들다

 

(2) TDD란 뭘까? Test-Driven-Development의 약자로, "테스트 주도 개발"이라는 테스트 방법론을 의미한다.

  : 간단히 요약하자면, 개발 전에 테스트 코드부터 작성하고 개발 후 리팩토링(수정)하는 방법이다.

 

(3) 테스트는 왜 해야할까? 비즈니스 로직에 맞고, 빠르고 안정적인 개발을 하기 위함

1) 테스트를 하면서 스스로 자신의 코드를 리뷰할 수 있다.

2) 테스트가 잘 되어있으면 리팩토링 하기가 수월하다.
  - 개발을 하다보면, 기존에 만들어진 코드가 정책에 맞춘 코드인지, 아니면 임의로 만들어진 코드인지 구분을 해야한다.
  - 테스트 코드를 보면 기획에서 다 알지 못하는 영역까지도 코드 내역을 통해 커버할 수 있다. 

 

(4) 테스트를 잘 하기 위해서는 뭘 해야할까?

1) 클래스나 메서드가 SRP를 지키며 너무 크지 X

2) 단위 테스트 시, 적절한 Mocking으로 격리성 확보 -- Mocking이란, 카피를 의미

3) 테스트 커버리지를 높혀, 놓치는 구간이 없도록 한다. 

4) 테스트 코드 또한 속도 및 방법 등의 면에서 지속적으로 개선해준다.

[ 테스트를 위한 라이브러리 - JUnit, Mockito ]

JUnit5 - xUnit이라는 단위 테스트(Unit Test) 프레임워크

- 단위 테스트를 실행 후 전체 결과를 리포트

- 스프링 2.4버전대부터 spring-boot-starter-test에 JUnit5가 포함되어 있다.
  * 최근 버전에서 JUnit4를 선택하면 테스트 동작안한다. (잦은 테스트 에러)
Mockito - Mock(가짜)을 만들어 주는 라이브러리

- 왜 필요한가? 
: 특정 controller나 service에 연관된 클래스들은 main에 작성되어 있다.
: 만약 test를 위해 main과 test를 둘 다 돌리면 port를 두 개의 프로그램이 사용하는 것으로 에러가 난다.
: 그렇기에 main에 있는 의존하는 클래스를 test 파일 안에 일일히 생성해주어야 하는데
: 이러한 방식은 품이 너무 많이 든다.
--> Mock을 만들어, 의존 클래스를 inject 한다.

[ JUnit5와 Mockito를 통한 단위 테스트 실습 ]

(1) JUnit5를 사용해보기

(1-1) build.gradle의 dependencies에 "spring-boot-starter-test"가 있는지 확인 (디폴트로 되어있다)

dependencies {
	// 생략
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

- 위에 보면 tasks.named('test')가 있는데, 'test'로 된 디렉토리의 테스트들을 JUnit으로 돌리겠다는 의미이다.

 

(1-2) 테스트하고자 하는 클래스에서 Ctrl+shirt+T로 테스트를 생성한다

- 보면 아래처럼 JUnit5로 라이브러리가 설정되어 있는 걸 볼 수 있다. 이걸 4로 바꾸면 당연히 테스트는 안 된다.

(1-3) @SpringBootTest와 @Autowired를 통해 의존을 주입한다.

// import 생략

@SpringBootTest      // 실제 환경과 동일하게 모든 Bean들을 등록
class CertainTest{
	
    @AutoWired // 주입
    CertainService certainService;
    
    @BeforeEach // 아래의 두 메소드 각각 시작 전에 행 하나 생성
    void init() { certainService.createOneRow(); }
    
    @Test
    @DisplayName("Test Name blah blah")
    void testGetOneRow(){
    	TableName tableName = certainService.getOneRow(1L);
        assertEquals("010-1234-5678", tableName.getPhone());
        assertEqauls(TableName.CertainType, tableName.getType());
    }
    
    
    @Test
    @DisplayName("Test Name blah blah2")
    void testGetOneRow2(){
    	TableName tableName = certainService.getOneRow(2L);
        assertEquals("010-1234-5678", tableName.getPhone());
        assertEqauls(TableName.CertainType, tableName.getType());
    }
}

- 그런데 위와 같이 하면 @SpringBootTest로 안 쓰는 클래스까지 가져오면 원하던 테스트에서 방향이 달라질 수 있다.

- 더불어 매번 @AutoWired로 주입을 하게되면 너무 품이 많이 든다.

- 이를 해소하고자 Mockito의 Mock을 사용한다.

 

(2) Mockito의 Mock을 사용해서 service에서 필요한 클래스만 가짜로 주입하기

@ExtendWith(MockitoExtension.class)   // MockitoExtension이라는 클래스를 쓴다.
class CertainServiceTest {

    @Mock // 가짜 생성
    private CertainRepository certainRepository;

    @InjectMocks // 위에서 만든 Mock을 주입
    private CertainService certainService;

    @Test
    @DisplayName("성공")
    void testXXX() {

        // given
        given(certainRepository.findById(anyLong()))
                .willReturn(Optional.of(TableName.builder()
                        .type(TableNameType.TYPE)
                        .phone("010-1234-5678").build()));

        // when
        TableName tableName = certainService.getOneRow(1234L);

        // then
        assertEquals("010-1234-5678", certainService.getPhone());
        assertEquals(TableNameType.TYPE, certainService.TableNameType());
    }
}

[ Controller 테스트 하기 ]

방법1 방법2
@SpringBootTest + @AutoConfigMockMvc

: 전체 Bean 생성 후

: mockMvc로 HTTP요청 및 검증




@WebMvcTest

: 필요로하는 MVC관련 Bean만 생성
 -  Controller, ControllerAdvice, Converter, Filter 등

: 하위 레이어 기능*의 경우, @MockBean으로 mocking
 - *Controller에 의존하는 Service 및 기타

: 마찬가지로 mockMVC로 HTTP 요청 및 검증
@WebMvcTest(AccountController.class)
class AccountControllerTest {
    @MockBean
    private AccountService accountService;

    @MockBean
    private RedisTestService redisTestService;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void successGetAccount() throws Exception{
        // given
        given(accountService.getAccount(anyLong()))
                .willReturn(Account.builder()
                        .accountNumber("3456")
                        .accountStatus(AccountStatus.IN_USE)
                        .build());
        // when
        // then
        mockMvc.perform(get("/account/876"))
                .andDo(print())
                .andExpect(jsonPath("$.accountNumber").value("3456"))
                .andExpect(jsonPath("$.accountStatus").value("IN_USE"))
                .andExpect(status().isOk());
    }
}

[ Service 테스트의 다양한 방법 ]

verify
의존하고 있는 Mock이 해당되는 동작을 수행했는지 확인하는 검증

verify(accountRepository, times(1)).save(any<Account>());
verify(accountRepository, times(0)).findById(anyLong());


ArgumentCaptor
의존하고 있는 Mock에 전달된 데이터가 내가 의도하는 데이터가 맞는지 검증

ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class);
verify(accountRepository, times(1)).save(captor.capture());
assertEquals("1234", captor.getValue().getAccountNumber());


assertions
다양한 단언(assertion) 방법들

assertEquals("1234", captor.getValue().getAccountNumber());
assertNotEquals("1234", captor.getValue().getAccountNumber());
assertNull(result);
assertNotNull(result);
assertTrue(resut.getBoolean());
assertFalse(resut.getBoolean());
assertAll(
() -> assertTrue(resut.getBoolean()),
() -> assertFalse(resut.getBoolean())
);


assertThrows
예외를 던지는 로직을 테스트하는 방법

AccountException exception =
assertThrows(AccountException.class, () ->
accountService.getAccount(123L));
assertEquals(ACCOUNT_NOT_FOUND, exception.getErrorCode());

 

 

[ 참조 및 출처 ]

부트캠프 수업 후 정리한 내용입니다.

HTTP 메시지 https://developer.mozilla.org/ko/docs/Web/HTTP/Messages

HTTP 2.0  https://velog.io/@taesunny/HTTP2HTTP-2.0-%EC%A0%95%EB%A6%AC

Lombok을 이용해 Builder 패턴 만들기 https://zorba91.tistory.com/298

H2 DB의 특징 https://jamie95.tistory.com/188

야믈 yaml 파일이란 https://www.inflearn.com/questions/16184

JPA란? https://dbjh.tistory.com/77?category=853400 

Layer 디자인 패턴 https://4ngeunlee.tistory.com/222

트랜잭션이란? https://wonit.tistory.com/462

Bean 생성주기 https://velog.io/@zenon8485/%EC%8A%A4%ED%94%84%EB%A7%81-%EA%B8%B0%EB%B3%B8-%EC%9B%90%EB%A6%AC-4.-%EC%8A%A4%ED%94%84%EB%A7%81-Bean%EC%9D%98-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0

단위테스트/통합테스트/인수테스트 https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/

TDD란? http://clipsoft.co.kr/wp/blog/tddtest-driven-development-%EB%B0%A9%EB%B2%95%EB%A1%A0/

 

|  예외처리

- 예외 : 프로그램이 예상하지 못한 상황을 만났을 때 오류를 처리하는 것

- 자바에서는 try-catch문 안에서 예외처리를 했었다.

try {
	// 행동
} catch (Exception e) {
	log.error("cetian exception");
}

- 스프링에서는 이러한 예외처리를 간편하게 할 수 있도록 하였다.

 

|  스프링의 예외처리 (REST API)

- 과거와는 달리 오늘날에는 프론트엔드와 백엔드 분업화가 잘 이루어져 있다.

- 백엔드에서는 보통 Rest API를 사용해 컨트롤러 기반의 예외처리를 주로 한다.

- 현장에서 예외처리를 할 때에는 enum과 예외객체를 통해 custom exception을 만들고,

 @RestControllerAdvice를 사용해서 예외처리를 한다.

 

[ Rest API를 활용한 컨트롤러 기반 예외 처리 ]

(1) @ExceptionHandler

- 컨트롤러에 예외 발생 시, 이를 처리해주는 메소드를 만들고 @ExceptionHandler를 표시

- 리스트 방식으로 @ExceptionHandler(IllegalAccessException e1, CustomeException e2) 넣을 수도 있다.

(2) HTTP Status code 변경 

@ResponseStatus - http 상태 코드를 Annotation으로 직접 지정

ex.@ResponseStatus(httpStatus.FORBIDDEN)
ResponseEntity - ResponseEntity에 예외객체를 제너릭으로 넣어 사용

ex. ResponseEntity<CustomException>

(3) 예외처리 우선순위

- 1. 해당 Exception이 정확히 지정된 Handler

- 2. 해당 Exception의 부모 예외 Handler

- 3. 또는 모든 예외의 부모 Exception

 

[ 어플리케이션의 전역적 예외 처리 - @RestControllerAdvice ]

- 방대한 양의 컨트롤러가 있는 프로그램에서 예외처리를 어떻게 해야할까?

- @RestControllerAdvice  :  Controller에게 사용되는 일종의 조언

  - 기존의 ControllerAdvice는 view를 응답하는 방식이었다면,

  - ResotControllerAdvice는 REST API용으로 객체를 응답하는 방식이다. (주로 JSON)

- 현재 개발자들에게 가장 많이 사용되고 있는 방식이라고 한다.

 

|  예외처리 실습

[1] Controller에 @ExceptionHandler로 예외처리 메소드를 만든다.

@ExceptionHandler(IllegalAccessException.class)
public String handleIllegalAccessException(
        IllegalAccessException e){

    log.error("IllegalAccessException is occured.", e);

    return "INVALID_ACCESS";
}

>> 동일 Controller 클래스 내 HTTP 매핑 메소드

@GetMapping("/order/{orderId}")
public String getOrder(
        @PathVariable("orderId") String orderId,
        @RequestParam("orderAmount") Integer orderAmount) throws IllegalAccessException {

    log.info("Get some order");

    if ("500".equals(orderId)) {
        throw new IllegalAccessException("500 is not valid orderId");
    }

    return "OrderId : " + orderId + ",  orderAmount : " + orderAmount;
}

>> 요청 및 응답

http://localhost:8080/order/500?orderAmount=10000

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Sun, 11 Sep 2022 01:24:40 GMT
Keep-Alive: timeout=60
Connection: keep-alive

INVALID_ACCESS

 

[2] @ResponsStatusHTTP 상태 코드를 설정한다.

- 참고 : HTTP 상태 코드 의미

1xx 정보 전달 : 요청 받았고, 작업 진행 중
2xx 성공 : 작업을 성공받으로 받았다.
3xx 리다이렉션 필요 : 짧은 코드의 경우 301이나 302코드 전송, 또는 헤더의 location에 이동할 url를 전송
4xx 클라이언트 오류 : 요청이 올바르지 않을 때

- 403 Forbidden (거부됨) : 관리자가 해당 사용자를 차단했거나 권한이 없거나, index.html이 없을 때
- 404 Not Found (찾을 수 없음) : 해당 리소스가 없음
- 410 Gone (사라짐) : 리소스가 영원히 사라진 경우
5xx 서버 오류 : 서버가 응답할 수 없을 때

- 500 Internal Server Error (내부 서버 오류) : 설정 및 퍼미션 오류, 또는 JSP/서블릿 등 호출 시 오류
- 502 Bad Gateway (게이트웨이 불량)

- 위를 보면 현재 페이지는 정상(HTTP/1.1 200)으로 나타나는데, 이를 변경하고 싶을 때 @ResponseStatus를 쓴다

@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(IllegalAccessException.class)
public String handleIllegalAccessException(
        IllegalAccessException e){

    log.error("IllegalAccessException is occured.", e);

    return "INVALID_ACCESS";
}

>> 요청 및 응답

http://localhost:8080/order/500?orderAmount=10000

HTTP/1.1 403 
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Sun, 11 Sep 2022 01:30:49 GMT
Keep-Alive: timeout=60
Connection: keep-alive

INVALID_ACCESS

 

[3] 예외객체를 만들어 JSON방식으로 예외 데이터를 전달한다.

- lombok을 활용하여 dto 안에 예외처리 객체를 만든다.

package com.example.websample.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class ErrorResponse {
    private String errorCode;
    private String message;
}

- 위에서 작성했던 예외처리 코드를 변경하여 객체로 묶는다.

@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(IllegalAccessException.class)
public ErrorResponse handleIllegalAccessException(
        IllegalAccessException e){

    log.error("IllegalAccessException is occured.", e);

    return new ErrorResponse("INVALID_ACCESS",
            "IllegalAccessException is occured.");
}

>> 요청 및 응답

http://localhost:8080/order/500?orderAmount=10000

HTTP/1.1 403 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 11 Sep 2022 01:37:26 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "errorCode": "INVALID_ACCESS",
  "message": "IllegalAccessException is occured."
}
Response file saved.
> 2022-09-11T103726.403.json

Response code: 403; Time: 513ms (513 ms); Content length: 77 bytes (77 B)

 

[4] ResponseEntinty를 사용하면 response에 헤더를 담을 수 있고, responseStatus를 지정할 수 있다.

@ExceptionHandler(IllegalAccessException.class)
public ResponseEntity<ErrorResponse> handleIllegalAccessException(
        IllegalAccessException e){

    log.error("IllegalAccessException is occured.", e);

    return ResponseEntity
            .status(HttpStatus.FORBIDDEN)
            .header("newHeader", "some value")
            .body(new ErrorResponse("INVALID_ACCESS",
                    "IllegalAccessException is occured."));
}

 

[5] 커스텀 예외처리 : enum예외객체를 사용하는 별도의 Custom Exception 클래스를 만든다.

- exception > ErrorCode (Enum)

package com.example.websample.exception;

public enum ErrorCode {
    TOO_BIG_ID_ERROR,
    TOO_SMALL_ID_ERROR
}

- dto > ErrorResponse (예외객체)

package com.example.websample.dto;

import com.example.websample.exception.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class ErrorResponse {
    private ErrorCode errorCode;
    private String message;
}

- exception > WebSampleException (예외처리 클래스)

package com.example.websample.exception;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor  // constructor
@Data                // getter/setter
public class WebSampleException extends RuntimeException{
    private ErrorCode errorCode;
    private String message;
}

>> 컨트롤러

@GetMapping("/order/{orderId}")
public String getOrder(
        @PathVariable("orderId") String orderId,
        @RequestParam("orderAmount") Integer orderAmount) throws WebSampleException {

    log.info("Get some order");

    if ("500".equals(orderId)) {
        throw new WebSampleException(
                ErrorCode.TOO_BIG_ID_ERROR,
                "500 is too big orderId");
    }

    if ("3".equals(orderId)) {
        throw new WebSampleException(
                ErrorCode.TOO_SMALL_ID_ERROR,
                "3 is too small orderId"
        );
    }

    return "OrderId : " + orderId + ",  orderAmount : " + orderAmount;
}

@ExceptionHandler(WebSampleException.class)
public ResponseEntity<ErrorResponse> handleWebSampleException(
        WebSampleException e){

    log.error("WebSampleException is occured.", e);

    return ResponseEntity
            .status(HttpStatus.INSUFFICIENT_STORAGE)
            .body(new ErrorResponse(ErrorCode.TOO_BIG_ID_ERROR,
                    "WebSampleException is occured."));
}

 

[6] @RestControllerAdvice 를 넣은 별도의 예외처리 클래스를 만든다.

- 사용법은 간단하다. 클래스에 @RestControllerAdvice를 넣고, 예외처리 핸들러 메소드를 모두 가져오면 된다.

- 각 컨트롤러는 지정한 예외(ex.WebSampleException)에 맞는 ExceptionHandler를 찾는다.

- 만약 없다면, 자신의 부모 예외를 찾고, 그도 없을 시에는 Exception을 찾아간다.

package com.example.websample.exception;

import com.example.websample.dto.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalAccessException.class)
    public ResponseEntity<ErrorResponse> handleIllegalAccessException(
            IllegalAccessException e){

        log.error("IllegalAccessException is occured.", e);

        return ResponseEntity
                .status(HttpStatus.FORBIDDEN)
                .body(new ErrorResponse(ErrorCode.TOO_BIG_ID_ERROR,
                        "IllegalAccessException is occured."));
    }

    @ExceptionHandler(WebSampleException.class)
    public ResponseEntity<ErrorResponse> handleWebSampleException(
            WebSampleException e){

        log.error("WebSampleException is occured.", e);

        return ResponseEntity
                .status(HttpStatus.INSUFFICIENT_STORAGE)
                .body(new ErrorResponse(ErrorCode.TOO_BIG_ID_ERROR,
                        "WebSampleException is occured."));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleWebSampleException(
            Exception e){

        log.error("WebSampleException is occured.", e);

        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse(ErrorCode.TOO_BIG_ID_ERROR,
                        "Exception is occured."));
    }
}

 

|  정리

- 오늘날의 개발 환경에서는 주로 REST API를 통한 예외처리전역적인 예외처리를 하는 것이 일반적이다.

- 개발자는 비즈니스로직에 맞는 커스텀 예외를 만들 수 있다.(Java의 다양한 예외를 상속)
  1) 커스텀 예외 Enum                                             -- ex. ErrorCode
  2) 전달할 예외 데이터 객체                                   -- ex. ErrorResponse
  3) 커스텀 에외 클래스                                           -- ex. WebSampleException extends RunTimeException

- 어플리케이션의 전역적인 예외처리를 위해서는,
  @RestControllerAdvice를 만들고, 예외 핸들러 메소드들을(@ExceptionHandler) 넣어준다.

 

 

[ 참고 및 출처 ]

- 부트캠프 수업을 들은 후 정리

- Http 응답 코드 https://namu.wiki/w/HTTP/%EC%9D%91%EB%8B%B5%20%EC%BD%94%EB%93%9C

|  MVC 구조로 보는 필터와 인터셉터

- 요청사항에 대해 필터와 인터셉터를 거친다 

출처:https://blog.naver.com/platinasnow/220035316135

- 사용되는 메소드의 전반적인 흐름

init -> doFilter -> preHandler -> AOP -> postHandler -> afterCompletion -> doFilter -> destroy

 

- 필터와 인터셉터와 AOP

  필터 인터셉터 AOP
공통점 공통처리 기능
관련 웹의 URL주소나, 프로토콜 등과 관련 자바코드(Annotation, package)
위치 스프링 외부  스프링 내부 스프링 내부
특징 - 대부분의 공통사항 필터에서 처리
- low level 처리 가능
- sql injection, CSRF 등 해킹을 사전에 막는용도로 자주 사용한다.
- Controller에 가기 전 공통 처리 
- 실제 매핑된 handler 정보 확인 가능
- 필터보다 상세한 조건식,
  세부적인 스펙(pre,post,after)
- 구체적인 시점에 구체적인 동작 가능
- 인터셉터보다 구체적인 조건
(ex. Annotation, parameter, 주소),
- 인터셉터보다 구체적인 동작위치
(ex. afterThrowing)
메소드 init
dofilter
destroy
preHandler
postHandler(응답성공)
afterCompletion(항상)
@before, @after,
@AfterReturning, @AfterThrowing
pointcut에 자유롭게 메소드 생성
더보기

[ 서블릿의 생명주기 ]

 

init() 서블릿을 처음 메모리에 올릴 때에만 한 번 실행
service() 요청 및 응답에 대한 처리 (GET이면 doGet(), POST면 doPost()로 분기)
destroy() 서블릿 종료 요청 시 실행

 

|  필터 실습

[1] config 패키지 안에 Filter 클래스를 만든다.

- Filter 클래스는 Filter 인터페이스를 구현하여 만들면 된다.

- Filter 클래스를 만들 때엔 doFilter() 메소드에서 chain.doFilter()를 꼭 해주어야 같은 필터로 인식한다.

// import는 생략

@Slf4j
public class LogFilter implements Filter{
	
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
                         throws IOException, ServletException {
        // 외부 -> filter (-> 처리 ->) filter -> 외부
        log.info("Hello LogFilter : " + Thread.currentThread());
        chain.doFilter(request, response); // chain.doFilter()는 꼭 필요
        log.info("Bye LogFilter : " + Thread.currentThread());
    }
}

[2] Filter 클래스를 구현할 때 방법 두가지

(1) @Component로 Filter 클래스를 스캐너가 자동 스캔할 수 있도록 하기

// import는 생략

@Slf4j
@Component
public class LogFilter implements Filter{
	
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
                         throws IOException, ServletException {
        // 외부 -> filter (-> 처리 ->) filter -> 외부
        log.info("Hello LogFilter : " + Thread.currentThread());
        chain.doFilter(request, response); // chain.doFilter()는 꼭 필요
        log.info("Bye LogFilter : " + Thread.currentThread());
    }
}

(2) 별도의 DI 설정 클래스에서 Filter클래스를 Bean으로 등록하기

- 위의 Filter 클래스에서 @Component는 생략한다.

- FilterRegistrationBean<Filter> 클래스를 통해 필터를 등록한다.

- FilterRegistrationBean의 메소드들

setFilter(new 클래스()) 필터를 세팅하기 (하나만 넣어준다)
setOrder(순번) 필터의 순번을 결정
addUrlPatterns("url패턴") 해당 패턴(ex. "/order", "/*") 의 url에서만 필터가 적용된다.
package com.example.websample.config;

// import 생략

@Configuration
public class WebConfig{
	
    @Bean
    public FilterRegistrationBean logginFilter(){
    	
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        
        return filterRegistrationBean;
    }
}

>> 기존에 컨트롤러는 아래의 것을 사용하고, 실행해보면

@GetMapping("/order/{orderId}")
public String getOrder(
        @PathVariable("orderId") String orderId,
        @RequestParam("orderAmount") Integer orderAmount)
{
    log.info("Get some order");
    return "OrderId : "+ orderId +",  orderAmount : " +  orderAmount;
}

>> 요청

http://localhost:8080/order/252?orderAmount=1000

>> 응답

OrderId : 252, orderAmount : 1000

>> 로그

com.example.websample.config.LogFilter   : Hello LogFilter : Thread[http-nio-8080-exec-1,5,main]
com.example.websample.config.LogFilter   : Bye LogFilter : Thread[http-nio-8080-exec-1,5,main]
c.e.w.controller.SampleController        : Get some order

 

|  인터셉터 실습

- 일반적으로 Filter로는 간단한 해킹 방지 코드를 작성하고, 인터셉터를 활용해서 세부적인 공통코드를 작성한다.

-  preHandle, postHandle(응답 성공), afterCompletion(항상)

[1] 인터셉터는 HandlerInterceptor 인터페이스를 구현하여 만들 수 있다.

packages com.exmaple.websample.config;

// import 생략

@Slf4j
public class LogInterceptor implements HandlerInterceptor{
	
}

[2] 인터셉터는 세가지의 메소드를 구현해야하는데, 각각의 특징은 아래와 같다.

메소드 언제? 매개변수
preHandle() Handler로 넘어가기 전 Object handler - handler의 위치를 알 수 있다.
postHandle() Handler의 처리가 성공적일 때 ModelAndView - 처리 후 응답 정보를 알 수 있다.
afterCompletion() Handler의 처리의 성공 여부와 상관 없이 항상 Exception ex - 예외 상황 발생 시 확인 가능
packages com.exmaple.websample.config;

// import 생략

@Slf4j
public class LogInterceptor implements HandlerInterceptor{
	@Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        log.info("preHandle LogInterceptor: " + Thread.currentThread());
        log.info("preHandle handler: " + handler);

        return true; // 이 다음 인터셉터가 진행되길 바라면 true, 아니면 false
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {

        log.info("postHandle LogInterceptor: " + Thread.currentThread());
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        log.info("afterHandle LogInterceptor: " + Thread.currentThread());

        if (ex != null) {
            log.error("afterCompletion exception : " + ex.getMessage());
        }

    }
}

[3] @Configuration 클래스에서 WebMvcConfigurer를 구현하고, addInterceptors()를 통해 인터셉터를 추가한다.

- WebMvcConfigurer의 addInterceptors()는 InterceptorResgistration을 반환한다.

- 내부적으로 아래와 같은 메소드들이 있는데 이들 모두 InterceptorResgistration을 반환하므로 메소드 체이닝을 한다.

- 메소드들

addInterceptor() 인터셉터 추가
order() 순번
addPathPatterns() 어떤 url패턴(경로)에서 실행할 건지
excludePathPatterns() 일반적으로 css나 images와 같이 정적 페이지들을 대상으로 제외처리
package com.example.websample.config;

// import 생략

@Configuration
public class WebConfig implements WebMvcConfigurer{
	
    // Filter 내용 생략
    
    // Interceptor 내용
    @Override
    public void addInterceptors(InterceptorRegistry registry){
    	// registry : 등록 내용 관리부
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/*", "/images/*");
    }
}

>> 기존의 컨트롤러에 예외 사항을 임의로 추가

@GetMapping("/order/{orderId}")
public String getOrder(
        @PathVariable("orderId") String orderId,
        @RequestParam("orderAmount") Integer orderAmount) throws IllegalAccessException {

    log.info("Get some order");

    if ("500".equals(orderId)) {
        throw new IllegalAccessException("500 is not valid orderId");
    }

    return "OrderId : " + orderId + ",  orderAmount : " + orderAmount;
}

>> 요청

http://localhost:8080/order/500?orderAmount=1000

>> 로그

// 필터 진입
Hello LogFilter : Thread[http-nio-8080-exec-1,5,main]

// preInterceptor
preHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
preHandle handler: com.example.websample.controller.SampleController#getOrder(String, Integer)

// handler
Get some order

// ! postInterceptor로 가지 않았다.

// afterInterceptor
afterHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
afterCompletion exception : 500 is not valid orderId

// exception 메세지 - 오류 코드 나머지는 생략
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalAccessException: 500 is not valid orderId] with root cause
java.lang.IllegalAccessException: 500 is not valid orderId

// 브라우저에 페이지 로딩하며 다시 실행
preHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
preHandle handler: org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
postHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]
afterHandle LogInterceptor: Thread[http-nio-8080-exec-1,5,main]

 

|  정리

- 필터, 인터셉터, AOP는 모두 공통 기능을 처리하기 위해 사용된다.

- 주로 필터, 인터셉터는 웹에 관한 처리(ex.URL주소 및 프로토콜)를 하며, AOP는 자바 코드에 관해 처리한다.

- 필터는 Filter 인터페이스를 구현하고, doFilter()안에서 chain.doFilter(request, response)를 작성하여 만들 수 있다.

- 인터셉터는 HandlerInterceptor 인터페이스를 구현하고, preHandle()/postHandle()/afterCompletion()을 작성하여 만들 수 있다.

- 필터와 인터셉터 모두 @Configuration 클래스에서 @Bean으로 등록해주는 것이 필요한데,

  - 필터의 경우, FilterRegistrationBean 클래스를 만들고 setFilter()/setOrder()/addUrlPatterns()를 설정한다.

  - 인터셉터의 경우, WebMvcConfigurer를 구현한 후, addInterceptor()를 통해 이미 등록된 registry의 메소드들을 사용한다.

 

 

[ 참조 및 출처 ]

부트캠프 수업 후 내용 정리

서블릿의 생명주기 https://kadosholy.tistory.com/47

[Spring] Interceptor, filter, AOP의 차이|작성자 심해펭귄

|  MVC

- MVC란? Model + View + Controller 의 약자를 말하며,

- 스프링은 내부적으로 Dispatcher가 있어, 어떤 요청이 있을 때마다 적절한 컨트롤러를 찾아 매핑한다고 했다.

- 이때 말하는 요청이란, 사용자가 보내는 HTTP 요청을 말하며 오늘날 웹 환경은 일반적으로 REST 방식을 따른다.

- REST는 URI의 자원에 고유한 이름-표현(ex. name, userId)-을 주어 자원의 상태 정보를 주고 받는 것으로,

  GET/POST/PUT/DELETE와 더불어 ?파라미터명=값 과 같이 자신만의 규칙을 갖고 있다.

- 스프링에서는 HTTP 요청을 해석하기 위해 RestController를 사용하며, 해당 컨트롤러의 메소드와 매핑하기 위해,

  RequestMapping 또는 축약형 Mapping (ex.@GetMapping)을 사용한다.

- HTTP의 패킷은 다른 프로토콜과 마찬가지로 Header와 Body를 가지고 있는데, 일반적으로 DB에 데이터를 추가하거나 수정할 때에는 이때 말하는 RequestHeader와 RequestBody를 통해 데이터를 읽고 소통한다.

@GetMapping @PostMapping @PutMapping @PatchMapping @DeleteMapping
데이터를 가져옴 데이터를 전송 전체 수정 일부 수정 삭제
@PathVariable (id)
@RequestParam
@RequestHeader
@RequestBody
@PathVariable (id)
@RequestParam

 

더보기

- Spring MVC의 흐름

출처:https://codingnotes.tistory.com/28
[1] Dispatcher Servlet이 요청 URL을 받아 HandlerMapping에 전달  * Dispatch : 파견 보내다. 
rf. 도서관의 사서(dispatcher)는 도서를 요청(request)받았고, 도서 코드 목록(HalderMapping)서 도서를 어떻게 찾을지 본다.

[2] HandlerMapping는 요청 URL에 맞는 Controller와 Method 정보를 반환
rf. 도서의 코드 목록에 따르면 해당 도서는 '역사' 카테고리 구역에 있단다.

[3] Dispatcher Servlet이 Handler Adapter에게 요청 처리를 위임  
rf. 사서는 인턴에게 '역사' 카테고리 책장으로 이동해서, 가나다순으로 진열된 책들을 보면 된다고 알려준다.

[4] HandlerAdapter가 Controller와 Method를 실행
rf. 인턴이 찾으려는 도서의 코드와, 사서의 안내를 가지고 '역사' 구역에 가서 도서를 찾는다.

[5] Cotroller는 비즈니스 로직을 처리하고, 그 결과를 바탕으로 뷰(ex.JSP)에 전달할 객체를 Model 객체에 저장
rf. 인턴은 찾은 도서를 가져와 책수레에 담는다.

[6] Dispatcher Servlet은 view name을 View Resolver에 전달하여 View 객체를 얻음
rf. 사서는 인턴으로부터 "00책 가져왔어?"라고 물어보고, 인턴은 "00책 여기있어요."하고 책수레에 있던 도서를 전달한다.

[7] Dispatcher Servlet은 View 객체에 화면 표시를 의뢰
rf. 사서는 책을 요청한 사람에게 책을 전달하기 위해 "00책 요청하신분~?"하고 물어본다. 

[8] View 객체는 해당하는 뷰(ex.JSP.Thymeleaf)를 호출하며,
    뷰는 Model 객체에서 화면 표시에 필요한 객체를 가져와 화면 표시를 처리
rf. 사서는 책을 요청한 사람에게 책을 대여해주고, 반납일에 대해 안내한다.

|  HTTP 요청 - RestController

- HTTP 요청 및 응답은 Rest 방식에 따른다.

구분 의미 SQL REST API
CREATE 삽입 INSERT PUT/POST
READ 조회 SELECT GET
UPDATE 갱신 UPDATE PUT/PATCH
DELETE 삭제 DELETE DELETE

- 데이터 타입은 일반적으로 JSON 타입을 많이 사용한다. (XML보다 경량)

- Controller와 RestController의 차이

Controller 기본적으로 HTML으로 응답값 전달
RestController Rest API 요청에 따른 응답값(ex.JSON) 전달

- 웹을 통해서 받는 URI의 경우 아래와 같이 [URL + 전달하려는 데이터]로 구성되어 있다.

URI 예시 
https://why-dev.tistory.com/252?category=964854

- RestController를 만들어주기 위해서는 아래와 같이 @RestController를 작성해주어야 한다.

@RestController
public class SampleController{

}

+ 여기에 만약에 롬복을 추가할 경우,

@Slf4j
@RestController
public class SampleController{

}

 

|  HTTP 요청 매핑

[1] Mapping Annotation

- 요청방식을 직접 지정 (GET,POST...)

// GET, POST 등의 요청방식을 직접 지정
@RequestMapping(value = "/page/252", method = RequestMethod.GET)
public String getPage(){
    log.info("Get some page information");
    return "page:252"; // JSON 방식
}

+ RequestMethod는 enum이다.

더보기
public enum RequestMethod {

   GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE

}

 

[2] 축약형 Mapping Annotation

- 최근에는 Mapping Annotation보다 축약형을 많이 사용한다.

@GetMapping @PostMapping @PutMapping @PatchMapping @DeleteMapping
데이터를 가져옴 데이터를 전송 전체 수정 일부 수정 삭제

- 아래와 같이 각 타입에 맞게 Annotation을 넣어주면 된다.

@GetMapping("/page/252") 
public String getPage(){
    log.info("Get some page");
    return "pageId : 252";
}

@PostMapping("/page")
public String createPage(){
    return "page created -> pageId : 252";
}

- 만약에 HTTP 전송에 대한 테스트를 하고 싶다면, 아래의 툴들을 이용할 수 있다.

인텔리J HTTP Client
POSTMAN https://www.postman.com/

인텔리J_HTTPClient
http-client예시

 

|  HTTP 요청 파라미터 전송

[1] Get과 Delete - @PathVariable, @RequestParam

(1) PathVariable : id를 path에 넣어 받는다

- 여기서의 id는 주문 번호(order id)와 같이 고유한 id 값을 의미한다.

- @PathVariable

@GetMapping("/{pageId}")
public String getCategory(@PathVariable("pageId") String pageId) {
    log.info("Get some page");
    return "pageId : " + pageId;
}

(2) query-params : 추가적인 정보들을 입력한다.

- @RequestParam

- 주로 게시판의 검색 필터 페이징에서 많이 사용한다.

- 만약에 추가적으로 필수사항이나 디폴트 옵션을 주고 싶다면 아래 키워드를 넣어줄 수도 있다.

@required  필수 입력 정보  * 기본적으로 true로 되어 있다. 
@defaultValue 디폴트 값 지정
@GetMapping("/{pageId}")
public String getPage(@PathVariable("pageId") String pageId,
                          @RequestParam("category") String category) {
    log.info("Get some page");
    return "pageId : " + pageId + ", category : " + category;
}

>> 요청

// 요청
GET http://localhost:8080/252?category=964854
Content-Type: application/json

>> 응답

// 응답
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 31
Date: Sat, 10 Sep 2022 17:58:31 GMT
Keep-Alive: timeout=60
Connection: keep-alive

pageId : 252, category : 964854

- Delete도 Get과 마찬가지로 PageVariable을 쓸 수 있다.

@DeleteMapping("{pageId}")
public String deletePage(@PathVariable String pageId){
    log.info("Delete some page");
    return "deleteId : " + pageId;
}
더보기

@require 사용 예시

@GetMapping("/{pageId}")
public String getPage(@PathVariable(value = "pageId") String pageId,
                      @RequestParam(value = "category", required = false) String category) {
    log.info("Get some page");
    return "pageId : " + pageId + ", category : " + category;
}

>> 요청

GET http://localhost:8080/252
Content-Type: application/json

>> 응답

pageId : 252, category : null

 

[2] Post, Put, Patch - @RequestBody, @RequestHeader

- CS 네트워크 시간에서 배운 HTTP 패킷의 구조를 생각나게 하는 Annotation이다.

@RequestHeader REST의 모든 메소드에서 사용 가능

- 메타정보, 인증키(토큰), 계정 정보 등을 담는다.
@RequestBody 실제 데이터. 일반적으로 JSON 방식으로 데이터를 받는다.

- JSON 방식을 사용하면 큰 데이터의 전송이 편하다.
@PostMapping("/order")
public String createPageOrder(
              @RequestBody CreateOrderRequest createOrderRequest,
              @RequestHeader String userAccountId) {
    log.info("Create order : " + createOrderRequest +
             ", userAccountId : " + userAccountId);
    return "orderId : " + createOrderRequest.getOrderId()
            + ", orderAmount : " + createOrderRequest.getOrderAmount();
}

@Data
public static class CreateOrderRequest{
    private String orderId;
    private Integer orderAmount;
}

>> 요청

POST http://localhost:8080/order
Content-Type: application/json
userAccountId: account800

{"orderId" : "123", "orderAmount" : 10000}

>> 응답

http://localhost:8080/order

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 34
Date: Sat, 10 Sep 2022 18:32:23 GMT
Keep-Alive: timeout=60
Connection: keep-alive

orderId : 123, orderAmount : 10000

 

|  정리

- HTTP 요청, 응답 시 REST방식에 따르며, 데이터 타입은 JSON을 일반적으로 많이 사용한다.

- HTTP Controller 클래스를 선언할 때엔 @RestController를 붙인다.

- HTTP Mapping을 할 때에는 @GetMapping, @PostMapping, @DeleteMapping.. 등의 축약형 매핑 Annotation을 쓴다.
* 이때, Put은 전체를 수정할 때, Patch는 일부를 수정할 때에 사용한다.

- HTTP 요청 파라미터를 전송할 때, 
  (1) Get과 Delete의 경우      - @PathVariable로 고유한 id를 받고, @RequestParam으로 parameter를 받는다.
  (2) Post, Put, Patch의 경우 - @RequestHeader@RequestBody를 통해 parameter를 받을 수 있다.
  * RequestBody를 쓸 경우, 큰 데이터를 쉽게 받을 수 있다.

 

 

[ 출처 및 참조 ]

부트 캠프 강의를 들은 후 정리

+ Recent posts