2024-06-11 ~ 2024-06-16
기초적인 CRUD product와 order service를 Instance 등록하였습니다.
프로젝트 실행 후, 아래의 링크에 접속해서 테스트할 수 있습니다.
http://localhost:8761/
커스텀은 다음의 링크를 참고하여 만들었습니다. 블로그 링크 / 깃허브 링크
API 명세서는 Swagger로 작성되어 있습니다.
프로젝트 실행 후, 아래의 링크에 접속해서 API를 테스트할 수 있습니다.
Order API : http://localhost:8081/swagger-ui/index.html
Product API : http://localhost:8082/swagger-ui/index.html
Spring Boot 2.7.15 (Maven) + Java17, MySQL 8.x, JPA, Spring Cloud Eureka
폴더 구조 작성 cmd 명령어 : 'tree > level.txt'
├─api-gateway
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─io
│ │ │ │ └─whatap
│ │ │ │ └─apigateway
├─order
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─io
│ │ │ │ └─whatap
│ │ │ │ └─order
│ │ │ │ ├─controller
│ │ │ │ ├─domain
│ │ │ │ ├─dto
│ │ │ │ │ ├─order
│ │ │ │ │ └─product
│ │ │ │ ├─global
│ │ │ │ │ ├─annotation
│ │ │ │ │ ├─client
│ │ │ │ │ ├─common
│ │ │ │ │ ├─config
│ │ │ │ │ ├─exception
│ │ │ │ │ │ └─error
│ │ │ │ │ └─swagger
│ │ │ │ ├─repository
│ │ │ │ └─service
├─product
│ ├─src
│ │ ├─main
│ │ │ ├─java
│ │ │ │ └─io
│ │ │ │ └─whatap
│ │ │ │ └─product
│ │ │ │ ├─controller
│ │ │ │ ├─domain
│ │ │ │ ├─dto
│ │ │ │ ├─global
│ │ │ │ │ ├─annotation
│ │ │ │ │ ├─common
│ │ │ │ │ ├─config
│ │ │ │ │ ├─exception
│ │ │ │ │ │ └─error
│ │ │ │ │ └─swagger
│ │ │ │ ├─repository
│ │ │ │ └─service
아래 링크에 들어가 내용을 참고하고 프로젝트를 실행시킬 수 있습니다. 자세한 내용 참고
Environment variables는 다음과 같습니다.
DB_HOST=;DB_NAME=;DB_PASSWORD=;DB_PORT=;DB_USERNAME=;DDL_AUTO=;
value | meaning |
---|---|
feat | 새로운 기능 추가 시 |
add | 새로운 클래스 추가 시 |
docs | 문서 변경 사항 추가 시 |
fix | 수정 사항 추가 시 |
refactor | 코드 품질 개선 시 |
move | 파일 또는 폴더 이동 시 |
test | 테스트 관련 사항 |
-
@SpringBootApplication이 수행하는 역할
-
ComponentScan이란?
@SpringBootApplication 어노테이션의 구조
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}), @Filter(type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})}) public @interface SpringBootApplication { ... }
-
커스텀 단순 설정
- @Target({ElementType.TYPE}) : TYPE = 클래스, 인터페이스 등에서 사용되는 어노테이션이라는 것을 정의
- @Retention(RetentionPolicy.RUNTIME) : 지속 시간 RUNTIME = 가장 긴 시간. 실행 시 사용.
- @Documented : javadoc에서 작성한 문서에 포함시키겠다.
- @Inherited : 자동 상속. 자식 클래스는 부모의 어노테이션을 사용할 수 있다.
-
중요 역할 3가지 어노테이션
-
@SpringBootConfiguration
계층 구조 @SpringBootApplication -------> @SpringBootConfiguration -------> @Configuration
- 스프링에서는 spring(=IoC) container 로 객체를 관리하는데, 이때 관리되는 객체를 Bean이라고 한다.
- @Configuration은 @Bean으로 정의된 메소드를 빈으로 등록하고 사용하는데 도움을 주는 어노테이션이다. → 설정 파일임을 말해줌
- ? : @Bean으로만 등록할 수 없을까? -> 가능은 하지만 싱글톤 보장이 없다.
-
@ComponentScan
- @Component가 붙은 (찾는 범위 지정 가능) 해당 범위의 클래스들을 찾아 스캔해서 자동으로 스프링 빈으로 등록해주는 어노테이션
- @Component가 붙은 (찾는 범위 지정 가능) 해당 범위의 클래스들을 찾아 스캔해서 자동으로 스프링 빈으로 등록해주는 어노테이션
-
@EnableAutoConfiguration
- auto-configuration 가능
- 기존 spring에서는 직접 XML 작성으로 configuration을 해야 했다.
- 하지만 spring boot에서는 classpath에 jar파일이 존재할 경우 자동으로 configuration을 해준다.
- 그렇다면 @ComponentScan과 @EnableAutoConfiguration 중 누가 먼저 실행되어 빈을 등록할까?
- 1단계 : @ComponentScan
- 2단계 : @EnableAutoConfiguration
- auto-configuration 가능
-
-
-
@Autowired 동작 과정 자세한 내용
- 객체 타입에 해당하는 빈을 찾아 자동 주입하는 역할
- 빈이란? 스프링에서 spring(=IoC) container가 관리하는 객체를 말한다.
- 그렇다면, 무엇으로 어떻게 빈을 찾아 주입하는 걸까?
- BeanPostProcessor 라는 라이프 사이클 인터페이스 구현체인 AutowiredAnnotationBeanPostProcessor에 의해 이루어진다.
- 객체 타입에 해당하는 빈을 찾아 자동 주입하는 역할
-
Spring Bean LifeCycle
- 빈의 라이프사이클은 크게 '객체생성 - 의존관계 설정 - 초기화 - 소멸' 순으로 나눈다.
빈 라이프사이클 도식화
- 파란색으로 표시한 부분이 BeanPostProcessor 내용입니다. BeanPostProcessor에는 2가지 메서드로
- postProcessBeforeInitialization : 빈 초기화 단계 이전 (의존관계 설정) -> 빈을 찾을 수 있는 메서드
- postProcessAfterInitialization : 빈 초기화 단계 이후 (소멸)
- 하지만, BeanPostProcessor도 인터페이스이기 때문에 구현체가 있어야 메서드를 이용할 수 있습니다.
BeanPostProcessor과 AutowiredAnnotationBeanPostProcessor
- AutowiredAnnotationBeanPostProcessor - processInjection(Object bean) 메서드 : 빈의 클래스 정보 읽어오는 getClass()로 메타데이터 얻고 주입 inject()를 실행시켜 주입을 합니다.
-> inject() 출처 : AutowiredAnnotationBeanPostProcessor -> InjectMetadata 상속 받는 : AutowiredFieldElement와 AutowiredMethodElement - 오버라이딩 inject()- inject() -> ReflectionUtils.makeAccessible() : 정보 + invoke() : 빈 주입
- inject() -> ReflectionUtils.makeAccessible() : 정보 + invoke() : 빈 주입
- 빈의 라이프사이클은 크게 '객체생성 - 의존관계 설정 - 초기화 - 소멸' 순으로 나눈다.
-
RestTemplate 자세한 내용
-
백엔드 서버에서 다른 서버로 통신해야 하는 경우, HTTP 통신 관련 코드 작성 또는 라이브러리 써야함.
-
자바 언어로 작성된 HTTP 통신용 클라이언트 라이브러리 ex. OkHttp, Retrofit 등 존재
-
스프링에서는 기본적으로 RestTemplate HTTP 클라이언트를 제공함
-
- 애플리케이션 내부에서 REST API에 요청하기 위해 RestTemplate의 메서드를 호출한다.
- HttpMessageConverter를 이용해 RequestEntity를 요청메시지로 변환
- ClientHttpRequestFactory로 ClientHttpRequest를 가져와 요청 보냄
- ClientHttpRequest는 요청 메시지를 만들어 HTTP 프로토콜을 통해 외부 서버와 통신
- ClientHttpResponse로 응답을 받고, ResponseErrorHandler에서 오류를 처리합니다.
- HttpMessageConverter를 이용해 응답메시지를 Java Object(Reseponse Type)로 변환하고
- 이를 애플리케이션에 반환합니다.
-
RestTemplate 메소드
메소드 HTTP 설명 getForObject GET HTTP GET 요청 후 객체로 응답 getForEntity GET HTTP GET 요청 후 ResponseEntity로 응답 postForLocation POST HTTP POST 요청 후 헤더에 저장된 URL을 응답 postForObject POST HTTP POST 요청 후 객체로 응답 postForEntity POST HTTP POST 요청 후 ResponseEntity 응답 delete DELETE HTTP DELETE 요청 headForHeaders HEADER HTTP HEAD 요청 후 헤더 정보 응답 put PUT HTTP PUT 요청 patchForObject PATCH HTTP PATCH 요청 후 객체 응답 optionsForAllow OPTIONS 지원하는 HTTP 메소드를 조회 exchange Any 원하는 HTTP 메소드 요청 후 ResponseEntity 응답 execute Any Request/Response 콜백 수정
- Github API 접근 예제
@GetMapping(value = "/github/{user}", produces = MediaType.APPLICATION_JSON_VALUE) public String githubUser(@PathVariable("user") String user) { // RestTemplate 생성 RestTemplate restTemplate = new RestTemplate(); // 요청 메시지 생성 및 설정 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // 요청 메시지에 바디 데이터가 없으므로 Void 타입으로 설정 RequestEntity<Void> requestEntity = new RequestEntity<>( null, headers, HttpMethod.GET, URI.create("https://api.github.com/users/" + user)); // 응답 메시지 ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class); // 응답 메시지의 바디 데이터를 문자열로 해석 String responseBody = response.getBody(); return responseBody;
-
6. @OneToMany, @ManyToOne : entity의 연관관계 지정해주는 어노테이션 - 엔티티들은 다양한 연관관계 지정 가능
- 방향
- 단방향 : 한 쪽의 엔티티만 참조하고 있음. (-> 나 <-)
- 양방향 : 양 쪽이 서로 참조하고 있음. (<->)
-
연관관계의 종류 및 어노테이션
연관관계 어노테이션 1 : 1 @OneToOne 1 : N @OneToMany N : 1 @ManyToOne N : M @ManyToMany
- 코드 작성 및 설명
-
예를 들어 한 사람이 여러 게시물을 만들 수 있다.
-> 한 사람 기준 one(1) - 여러 게시물 기준 many(n)@Entity public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne // post - many, user - one @JoinColumn(name = "user_id") private User user; } @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy = "user") // user - one, post - many private List<Post> posts = new ArrayList<>(); }
- 의문점 : 연관관계 작성할 때 @ManyToOne, @OneToMany만 있으면 될 것 같습니다. -> @JoinColumn과 mappedBy는 왜 작성하셨나요?
- 게시글은 누가 작성했는지 User의 값을 알 수 있고, 반대로 User는 List로 어떤 게시물들을 작성했는지 알 수 있습니다.
- 그런데 가정을 해보도록 하겠습니다. 만약 유저가 게시물 하나를 작성했습니다.
- 게시물 작성하여 이제 데이터베이스에 이 내용을 저장해야합니다.
- 그렇다면 Post 클래스 - User를 넣어야할까요? / User 클래스 - List에 넣어야할까요?
- JPA 입장에서는 혼란이 올 수 밖에 없습니다. 따라서 누굴 우선으로 할지 주인을 결정 해야 합니다.
- @ManyToOne = 1개의 외래키만 가지고 있음 > @OneToMany = List 여러개의 값을 가지고 있음.
-> 즉, 관리면에서 한개의 외래키만 가지고 있는 것이 효율적임. - 주인이라면 : @JoinColumn / 주인이 아니라면 : mappedBy 를 작성합니다.
- @JoinColumn : 외래 키를 매핑할 때 사용 (name = : 외래키 컬럼명 지정)
- name = : 단순히 컬럼명만 지정
- referencedColumnName = : 대상 테이블의 어떤 컬럼을 참조할 것인지 지정
- mappedBy : 연관관계의 주인을 지정 (단, 연관관계 주인의 해당 속성 필드명과 일치)
- @ManyToOne = 1개의 외래키만 가지고 있음 > @OneToMany = List 여러개의 값을 가지고 있음.
- 그 밖의 편의 옵션
-
프록시 옵션 : fetch
- DB 조회 시, 연관관계의 엔티티 정보를 언제 요청할 것인지
- LAZY : 데이터가 필요한 시점에 정보를 요청함. (즉, 쿼리를 2번 보냄)
- EAGER : 조인으로 바로 모든 정보를 요청함. (즉, 쿼리는 1번 + @OneToOne, @ManyToOne 어노테이션 기본 설정값)
- 특별히 반드시 가져와야 할 정보가 아니라면 LAZY를 선택하는 것이 좋음.
-
영속성 옵션 : cascade
-
-
Entity
-
Entity와 DTO는 분리하는 이유
- 사용자가 필요한 데이터만 DTO에 전달하기
- JSON 직렬화 이슈 해결
- 민감한 정보는 노출되지 않는 보안성 강화
-> Entity와 DTO 간 변환 메서드 구현하기 (toEntity() <-> toDto())
-
Entity로 선언된 클래스는 모든 필드를 컬럼으로 취급
-> @Column(name=)은 클래스의 필드명과 데이터베이스의 필드명이 다를 경우에 사용하기
-
생성자와 getter, setter 어노테이션 -> Lombok 이용하기
- 생성자
-
파라미터가 없는 생성자 : @NoArgsConstructor
단, 접근 제어자는 PROTECTED로 하는 이유
-> (즉시 로딩 제외) 자연 로딩(LAZY) 인 경우 실제 엔티티가 아닌 프록시 객체를 통해서 조회- private인 경우 : 프록시 객체 생성에 접근 불가능
- public인 경우 : 무분별한 객체 생성 초래 + setter로 통한 값 주입 -> 값 변경 추적 어려움
-
모든 파라미터가 있는 생성자
- @AllArgsConstructor 문제점
- Builder 패턴 은 무엇이고 사용하는 이유
-> '첫번째 < 두번째'
-
- 생성자
-
PK 매핑 전략 자세한 내용
- AUTO : 데이터베이스에 맞는 자동 키 생성. (ex. MySQL : AUTO_INCREMENT)
- IDENTIFIED : insert 시 자동으로 id 키 값 증가
- SEQUENCE : 시퀀스를 이용하여 기본 키 생성
- UUID : UUID를 이용하여 기본 키 생성
-
BaseTimeEntity : 등록 날짜, 수정 날짜
- @MappedSuperclass : 상속할 경우, 해당 클래스의 필드도 컬럼으로 취급
- @EntityListeners(AuditingEntityListener.class) : 엔티티 생성 또는 수정 시간 등을 파악해서 자동 저장
- @CreatedDate : 엔티티가 생성되는 시간 자동 저장
- @LastModifiedDate : 엔티티가 수정될 때마다 시간 자동 저장
-
-
Repository
- @Repository 생략
- @Service, @RestController 등 각 계층마다 어노테이션이 있는 것을 확인할 수 있음.
- @Repository는 생략 가능한 이유
- 기본적으로 @Repository를 생략하면 빈으로 등록되지 않음
- 하지만, JpaRepository를 상속받을 경우 의존성 주입 가능한 이유
- @NoRepositoryBean : 실제 구현체는 SimpleJpaRepository = @Repository
-
Exception
-
종류
- ErrorCode : Enum 클래스로 상수 HTTP 상태 코드와 메시지를 가질 수 있음
ex. BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다.") - ErrorResponse : HTTP 상태 코드와 메시지 반환할 수 있는 response 클래스
- BusinessException : 상속받아 구체화 된 하위 exception 클래스를 가지고 있음. (parent)
- 하위Exception : BusinessException을 상속받아 각자 다른 (상태 코드와 메시지 = ErrorCode enum) 을 가지고 있음. (child)
- GlobalExceptionHandler : 전역 exception을 핸들링 할 수 있는 클래스
- BusinessException : 구체화 된 하위 exception 클래스까지 핸들링할 수 있음.
- MethodArgumentNotValidException : 유효성 검사 exception 클래스 핸들링
- ErrorCode : Enum 클래스로 상수 HTTP 상태 코드와 메시지를 가질 수 있음
-
- 예외 발생 경로 trace : 예외 만들며 1~5ms 소비
- 서비스의 복잡도에 따라 stack depth 깊어짐
-> fillInStackTrace() 오버라이딩하여 trace 비활성화하기
-
-
더미 데이터
- data.sql, @PostConstruct, @EventListener(ApplicationReadyEvent.class)
- @PostConstruct vs @EventListener(ApplicationReadyEvent.class) 참고 내용
- @PostConstruct : 클래스가 생성된 직후 초기화 메서드가 실행
- 문제 : AOP가 적용된 상태에서 동작해야 하는 초기화 작업을 해야할 때.
-> AOP는 원본 클래스 그대로가 아닌, 상속으로 Proxy 클래스르 만든 후, Proxy 클래스를 사용함.
-> 그렇다면 Proxy 클래스의 초기화 메서드를 수행해야 만 원하는 동작이 수행되는데, @PostConstruct는 그렇지 않음.
- 문제 : AOP가 적용된 상태에서 동작해야 하는 초기화 작업을 해야할 때.
- @EventListener(ApplicationReadyEvent.class) : 특정 이벤트 발생 시점에서 초기화 메서드가 실행
- ApplicationReadyEvent.class : Spring이 모든 초기화가 종료된 시점에서 발생 -> 즉, Proxy 클래스의 생성도 모두 마친 상태.
- 또한, @PostConstruct와 달리 @Transactional 어노테이션을 함께 사용 가능함.
- @PostConstruct : 클래스가 생성된 직후 초기화 메서드가 실행
-
Netflix
- Eureka : 다수의 서비스들의 미들웨어서버
- 사용 목적
- 로드 밸런싱 : 특정 서비스가 여러개 있을 때, 트래픽을 한 서버에 몰리지 않게 분산해주는 기술
- 장애 조치 목적
- 미들웨어란? 데이터를 주고 받는 양쪽의 서비스 중간에 위치해 매개 역할을 하는 소프트웨어
- 사용 목적
- Feign Client : Http Client - Http 요청 간편함
- 장점
- Feign Client은 Http Client보다 더 간편함.
- 통합 테스트가 비교적 간편
- 사용자 의도에 맞는 커스텀이 간편함
- 프로젝트 적용
- Order에서 Product API 요청 시,
- GET /api/product/{id} : product의 id로 product 정보 요청
- PUT /api/product/stock/{id} + UpdateInventoryProductRequest : product의 id로 product inventory(재고량) 변경 요청
@FeignClient(name = "product", url = "http://localhost:8082/api/product") public interface ProductClient { @GetMapping("/{id}") ProductResponse findByProductId(@PathVariable("id") Long id); @PutMapping("/stock/{id}") ProductResponse updateStockByProductId(@PathVariable("id") Long id, @RequestBody @Valid UpdateInventoryProductRequest request); }
- Order에서 Product API 요청 시,
- 장점
- Eureka : 다수의 서비스들의 미들웨어서버