1. 새롭게 배운 내용들
(1) ⭐️⭐️ 외부 API 사용할 때는? Rest Client!
- ⭐️ 스프링 프레임워크를 사용한 애플리케이션에서 외부 API와 통신할 수 있는 방법은 무엇이 있을까?
- 통신할 수 있는 방법인 HTTP 클라이언트는 RestClient, WebClient, RestTemplate이 있다.
- RestClient란?
- 스프링 6.1에 도입된 동기식 HTTP 클라이언트 애플리케이션. 템플릿 메서드 API를 제공하는 RestTemplate과는 달리 보다 유연한 API를 제공한다.
- ⭐️ Java 객체를 편리하게 HTTP 요청에 매핑하고, HTTP 응답을 Java 객체로 변환하여 처리할 수 있다.
- ⭐️ ⭐️ 따라서 핵심은 외부 API 명세서를 잘 읽어보고 Java 객체를 어떻게 HTTP 요청에 매핑할지, 어떻게 HTTP 응답을 Java 객체로 변환시킬지라고 볼 수 있다!!
- RestClient 활용
- 일단, builder 혹은 create 메서드를 사용하여 생성되면 여러 스레드에서 안전하게 사용할 수 있다.
- 예시
- HTTP 메서드 지정 : 어떤 HTTP 메서드를 사용할지 정할 수 있다. get( ), post() 등등..
- 요청 URI 설정 : uri 메서드는 기본적으로 URI 타입을 인자로 받기에 URI.create(url)을 통해 문자열을 URI 객체로 변환해주었다.
- Content-Type 설정 : 요청할 때의 데이터 형식을 정해주는 contentType 또한 설정 가능하다.
- Requestheader와 Requestbody 설정
- 응답 처리 : HTTP response 또한 retrieve() 메서드를 통해 받을 수 있다.
- 응답 변환 : 응답을 어떠한 형태로 변환할지 정할 수 있다.
Pet pet = ...
String ulr = "https://~~";
ResponseEntity<String> response = restClient.post() // 1.
.uri(URI.create(url)) // 2.
.contentType(APPLICATION_JSON) // 3.
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) // 4.
.body(pet) // 4.
.retrieve() // 5.
.toEntity(String.class); // 6.
(2) 중복 코드를 분리하는 @MappedSuperClass~~!!!
JPA에서 제공하는 애노테이션으로 공통적으로 사용되는 필드나 메서드를 상속받을 수 있도록 하는 기능을 제공한다. 엔티티 클래스들의 공통 속성들을 모아두는 추상 클래스에 주로 사용되며 이 추상 클래스는 테이블과 매핑되지 않는다. 또한 이를 상속받는 엔티티 클래스는 추상 클래스의 필드와 메서드를 상속 받게 된다.
1. 공통 속성을 가지는 추상 클래스 정의하기
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Column;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class BaseEntity {
@Column(name = "created_at")
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Getters and setters
...
}
2. 엔티티 클래스에서 상속 받기
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class User extends BaseEntity {
@Id
private Long id;
private String username;
// Getters and setters
...
}
@Entity
public class Product extends BaseEntity {
@Id
private Long id;
private String name;
// Getters and setters
...
}
- 단, 추상 클래스를 상속 받는다고 해서 필드 값을 공유하는 것은 아니다. 그저 구조와 행위를 재사용하는 방법일 뿐, 각 인스턴스는 자신의 고유한 필드값을 가진다.
(3) REST API
- API란 무엇일까? 응답과 요청이 정해져있는, 마치 메뉴판과 같은 것을 우리는 API라고 부른다.
- 그렇다면 REST API란 도대체 무엇일까?
- RESTful한 API로서 2000년 Roy Thomas Fielding이 쓴 논문에서 처음으로 등장한 개념이다.
- REST : 웹과 같은 분산 하이퍼미디어 시스템을 위한 아키텍처 스타일
- 꼭 REST API에 맞춰서 API를 만들어야할까?
- 필딩 가라사대 : 시스템을 완전히 통제할 수 있다고 생각한다면 REST에 시간을 낭비하지 말라! REST API는 표준적인 방법이기에 매번 특정 상황에 최적될 수 없기 때문이다.
- 참고자료
- REST API의 특징
- ⭐️ Uniform-Interface (REST API인지 나뉘는 가장 큰 기준)
- 독립적인 인터페이스이기에 http와 html의 발전에 의해 API가 업데이트되거나 영향을 받으면 안된다.
- 또한 각 자원들이 다음 4가지 조건들을 충족해야 한다.
- (1) URL 자원 식별
- 자원 : 이름을 지닐 수 있는, 개념적인 대상 ex) 문서, 이미지 등
- 자원은 객체처럼 상태가 변화하기에 변하지 않는 식별자가 필요하다.
- ⭐️ 따라서 자원은 url로 식별되어야 한다. ex) /product/1201와 같은 엔드포인트로서 식별됨
- (2) 표현을 통한 자원조작
- 자원은 개념적인 대상이기에 다양한 방식으로 표현 가능하다.
- ⭐️ url과 GET, DELETE, POST 등 HTTP 표준 메서드 등으로 자원을 조회, 삭제, 생성하는, 자원 조작을 설명할 수 있는 정보가 담겨야 한다.
- 예를 들어 상품을 가져온다고 해서 url을 getProduct로 하는 것이 아닌 http 메서드를 GET으로 쓰고 url을 product로 설정해야 한다.
- (3) Self-descriptive messages
- 메서지는 스스로에 대해 설명해야 한다. (클라이언트와 서버 사이의 컴포넌트들에게)
- ⭐️ 그래야 컴포넌트들이 메시지의 내용을 참고하여 적절한 작업을 수행한다.
- 설명 중 하나 ex) Host 헤더에 도메인명을 반드시 기재해야 한다. IP 주소로 도메인명을 대체할 수 있지 않냐는 생각이 들겠지만 하나의 IP 주소에 복수의 도메인명이 존재하기에 IP 주소만으로는 요청 대상을 찾아낼 수 없다.
- (4) HATEOAS 구조
- 백엔드 입장에서 프론트에 보내는 JSON 반환값에 다른 API를 포함하여 보내야 한다.
- Stateless
- ⭐️ HTTP 자체가 Stateless하기에 HTTP를 이용하는 것만으로도 만족한다.
- Cacheable
- HTTP 헤더의 cache-control = public이 default이기에 HTTP 자체는 기본적으로 캐싱이 된다. 단, HTTP 메서드 중 GET에 한정된다.
- Client-Server 구조
- 클라이언트와 서버는 독립적인 구조를 가져야 한다. 이 또한 클라이언트에서 HTTP로 받는 로직만 잘 처리하면 만족한다.
- Layered System
- Code-on-Demand (optional)
- REST API의 URI 규칙
- ⭐️⭐️ 동작은 HTTP 메서드만으로만 하고 url에 해당 내용이 들어가면 안된다.
- .jpg, .png 등 확장자는 표시하면 안된다.
- ⭐️ 동사가 아닌 명사로만 표기해야 한다. 예를 들어 유저가 아파트를 가진다면 /users/{userId}/aparts로 나타낸다.
- ⭐️ 계층적인 내용을 담아야 한다. '집/아파트/전세' 이런 식으로 내려가야 한다.
- ⭐️ 대문자가 아닌 소문자로만 쓰고 너무 길경우 '바-'를 사용한다.
- ⭐️ HTTP 응답 상태 코드를 적재적소에 활용한다.
ex)
1. 도서관 시스템의 REST API를 만든다.
모든 책 조회 : GET books/
책을 생성 : Post books/{booksid}
책을 수정 : PUT books/{booksid}
어떤 유저가 특정 책을 빌림 : PUT users/{userid}/books/{booksid}
2. 쿼리 스트링과 혼합된 url
실제 워드프레스에서 제공하는 REST API - posts의 2번째 결과물을 가져온다.
GET /wp/v2/posts?page=2
(api를 설정할 떄 v2, v1으로 버전을 명시해주면 마이그레이션하기에 유리하다!!)
(4) ⭐️⭐️ 트랜잭션
- 트랜잭션이란?
- 일반적으로 관련 로직이 모두 성공적으로 완료되어야 하는 일련의 작업
- 하나 이상의 작업이 실패한다면 다른 모든 작업이 취소(rollback)되어 애플리케이션 상태가 변경되지 않는다.
- 애플리케이션에서 데이터 무결성을 보장하기 위해 필요하다!
- 트랜잭션을 정의하는 4가지 중대한 속성, ACID
- Atomicity (원자성) : 모든 변경을 수행하거나 아무것도 수행하지 않는다.
- Consisitency (일관성) : 트랜잭션 처리 전과 처리 후 데이터 모순이 없는 상태를 유지해야 한다.
- Isolation (독립성) : 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장해야 한다.
- Durablitiy (지속성) : 성공적으로 수행된 트랜잭션은 영원히 반영되어야 한다.
- 프록시 패턴
- 실제 객체에 대해 참조하여 클라리언트가 실제 객체 대신 프록시 객체를 통해 작업을 수행하게 한다. 이를 통해 실제 객체에 대한 접근을 제어할 수 있으며, 요청이 실제 객체에 전달되기 전 프록시 객체를 통해 로직을 수행할 수 있다.
- 주로 행하는 작업들 : 초기화 지연, 로깅, 액세스 제어, 캐싱 등
- 활용 1. 트랜잭션의 전파(Propagation) 속성 설정
- 해당 메서드가 호출될 때 트랜잭션을 어떻게 처리할지 결정하는데 사용한다.
- REQUIRED (대부분 요거)
- 호출된 메서드가 트랜잭션 안에서 실행되어야 한다.
- 진행중인 트랜잭션이 있다면, 해당 트랜잭션 내에서 실행하고 진행중인 트랜잭션이 없으면 새로운 트랜잭션을 시작한다.
- SUPPORTS (트랜잭션이 존재하지는 않지만, 있으면 사용하도록 하는 경우에 사용됨)
- 메서드가 트랜잭션 안에서 실행될 필요는 없지만 이미 진행중인 트랜잭션이 있다면, 해당 트랜잭션 내에서 실행한다. 트랜잭션이 없다면 비트랜잭션 모드에서 실행한다.
- MANDATORY (트랜잭션이 무조건 필요하며 존재하지 않으면 exception throw!!)
- 메서드가 반드시 트랜잭션 안에서 실행되어야 함을 나타낸다. 만약, 이미 진행 중인 트랜잭션이 없다면 예외를 발생시킨다.
- NEVER (트랜잭션 없이 실행되어야 하는 메서드에 사용)
- 메서드가 트랜잭션 안에서 실행되면 안됨을 나타낸다.
- REQUIRES_NEW (독립적으로 실행되어야 하는 작업에 사용된다)
- 메서드가 항상 새로운 트랜잭션을 시작하도록 한다. 이미 진행 중인 트랜잭션이 있다면 잠시 일시중지하고 새로운 트랜잭션을 시작한다.
- 활용 2. 트랜잭션끼리 서로 간섭할 수 있는 정도를 나타내는, 격리(Isolation) 수준 설정
- READ_UNCOMMITTED
- 가장 낮은 격리 수준으로, 트랜잭션이 다른 트랜잭션의 commit되지 않은 변경 사항(더티 데이터)를 읽을 수 있다. 이는 Dirty Read를 허용하며, 가장 많은 동시 액세스를 허용하지만 일관성 문제를 일으킬 수 있다.
- 특히 행을 다시 읽거나(Non-repeatable Read) 범위 쿼리를 다시 실행할 때(Phantom Read) 다른 결과를 얻을 수 있다.
- READ_COMMITED (PostgresSQL의 기본 격리 수준)
- 다른 트랜잭션의 commit되지 않은 변경 사항을 읽지 못하게 한다. Dirty Read를 방지한다.
- 단, 다른 트랜잭션이 변경 사항을 커밋하면 다시 쿼리하여 결과가 변경될 수 있다.
- REPEATABLE_READ
- 트랜잭션이 시작된 이후 다른 트랜잭션에서 커밋된 변경 사항에 영향을 받지 않도록 한다.
- 그러나 범위 쿼리를 다시 실행할 때 새로 추가되거나 제거된 행을 얻을 수 있다. (Phantom Read)
- SERIALIZABLE
- 가장 높은 격리 수준으로, 트랜잭션을 직렬화하여 실행하는 것처럼 처리한다. 즉, 동시 호출을 순차적으로 실행하여 일관성을 최대로 높인다.
- 덕분에 Dirty Read, Non-repeatable Read, Pahntom Read를 방지할 수 있지만 낮은 동시성으로 성능이 떨어질 수 있다.
- READ_UNCOMMITTED
- 활용 3. 트랜잭션이 완료될 때까지 기다릴 최대 시간 지정 (Timeout)
- 무한정 대기하는 것을 방지하고, 트랜잭션의 응답 시간을 제어하기 위해 사용된다.
- 활용 4. ReadOnly
- 트랜잭션이 읽기 전용임을 나타낸다. 성능 최적화를 읽기 전용 트랜잭션을 명시하고, 데이터 변경을 방지한다.
- 활용 5. Rollbakc Rules
- 트랜잭션이 롤백되어야 하는 예외 상황을 정의한다. 기본적으로 런타임 예외가 발생하는 상황에서는 트랜잭션이 자동으로 롤백되는데, 체크드 예외가 발생하면 롤백되지 않고 커밋된다. 이러한 상황에서 체크드 예외에서도 롤백하도록 설정할 수 있다.
- 트랜잭션 격리 수준에서 발생할 수 있는 에러들
1. Dirty Read : 한 트랜잭션이 다른 트랜잭션의 커밋되지 않은 변경사항을 읽을 때 발생한다.
트랜잭션 A: 한 사용자가 계좌의 잔액을 업데이트합니다.
트랜잭션 B: 다른 사용자가 같은 계좌의 잔액을 조회합니다.
1. 트랜잭션 A가 계좌 잔액을 $100에서 $200으로 업데이트합니다.
UPDATE accounts SET balance = 200 WHERE id = 1;
2. 트랜잭션 B가 같은 계좌의 잔액을 조회합니다. (이 시점에서 트랜잭션 A는 아직 커밋되지 않았음)
SELECT balance FROM accounts WHERE id = 1; -- 결과: 200 (Dirty Read)
3. 트랜잭션 A가 롤백됩니다.
ROLLBACK;
4. 트랜잭션 B가 다시 같은 계좌의 잔액을 조회합니다.
SELECT balance FROM accounts WHERE id = 1; -- 결과: 100
2. Non-repeatable Read : 동일한 트랜잭션 내에서 데이터를 두 번 읽을 때, 중간에 다른 트랜잭션이 해당 데이터를 변경하여 읽을 때마다 다른 결과가 나오는 상황 발생
트랜잭션 A: 계좌의 잔액을 조회합니다.
트랜잭션 B: 같은 계좌의 잔액을 업데이트합니다.
1. 트랜잭션 A가 계좌 잔액을 조회합니다.
SELECT balance FROM accounts WHERE id = 1; -- 결과: 100
2. 트랜잭션 B가 계좌 잔액을 $100에서 $200으로 업데이트하고 커밋합니다.
UPDATE accounts SET balance = 200 WHERE id = 1;
COMMIT;
3. 트랜잭션 A가 다시 계좌 잔액을 조회합니다.
SELECT balance FROM accounts WHERE id = 1; -- 결과: 200 (Non-repeatable Read)
3. Phantom Read : 동일한 트랜잭션 내에서 같은 조건의 범위 쿼리를 두 번 실행할 때, 중간에 다른 트랜잭션이 새로운 행을 삽입하거나 삭제하여 결과가 달라지는 상황 발생
트랜잭션 A: 특정 조건을 만족하는 행을 조회합니다.
트랜잭션 B: 같은 조건을 만족하는 새로운 행을 삽입합니다.
1. 트랜잭션 A가 특정 조건을 만족하는 모든 계좌를 조회합니다.
SELECT * FROM accounts WHERE balance > 100; -- 결과: 2개의 행
2. 트랜잭션 B가 새로운 계좌를 삽입합니다.
INSERT INTO accounts (id, balance) VALUES (3, 150);
COMMIT;
3. 트랜잭션 A가 다시 같은 조건으로 조회합니다.
SELECT * FROM accounts WHERE balance > 100; -- 결과: 3개의 행 (Phantom Read)
(5) ⭐️⭐️ 트랜잭션과 데이터베이스 관리 최적화
- 데이터베이스와 외부 API를 사용하여 애플리케이션에서 트랜잭션 관리는 매우 중요하다. 특히 외부 API 호출에 오랜 시간이 걸리면 데이터베이스 커넥션 풀이 소진될 수 있어 성능 저하나 시스템 오류가 초래될 수 있다.
- Database Connection Pool : 애플리케이션에서 DB와의 연결을 효율적으로 관리하기 위해 사용되는 기술. DB 연결을 미리 일정 수만큼 만들어 두고, 필요할 때마다 이 연결을 재사용하는 방식으로 동작한다.
- 문제 상황의 예시 - DB 커넥션 풀의 소진
@Transactional
public void initialPayment(PaymentRequest request) {
savePaymentRequest(request); // DB
callThePaymentProviderApi(request); // API
updatePaymentState(request); // DB
saveHistoryForAuditing(request); // DB
}
위와 같은 메서드가 있다고 가정해보자. 이 메서드는 @Transactional 애노테이션을 통해 트랜잭션을 관리하고 있다. 트랜잭션이 시작되면 DB 커넥션이 할당되고, 트랜잭션이 끝날 때까지 유지된다. 따라서 ⭐️⭐️⭐️ 외부 API 호출이 오래 걸리게 되면 DB 커넥션을 오랜 시간 점유하기에 다른 작업들이 커넥션을 얻지 못해 장시간 대기하여 성능 저하 문제를 초래하거나 애플리케이션 자체가 실패할 수 있다. 여기서 ⭐️⭐️⭐️ 다른 작업들이란 updatePaymentState(request)와 saveHistoryForAuditing(request) 뿐만 아니라 애플리케이션 내에서 실행되고 있는 다른 트랜잭션들의 작업까지 모두 포함한다.
- 해결방법
public void initialPayment(PaymentRequest request) {
savePaymentRequest(request); // 첫 번째 DB 호출
// 트랜잭션 밖에서 API 호출
callThePaymentProviderApi(request); // API 호출
// 트랜잭션 안에서 나머지 작업 수행
updatePaymentAndHistory(request);
}
@Transactional
public void updatePaymentAndHistory(PaymentRequest request) {
updatePaymentState(request); // 두 번째 DB 호출
saveHistoryForAuditing(request); // 세 번째 DB 호출
}
⭐️⭐️ 위와 같이 외부 API 호출을 트랜잭션 밖으로 이동시켜 DB 커넥션 점유 시간을 줄이는 것이 좋다. 이를 통해 외부 API 호출 작업이 독립적으로 실행되어 DB 커넥션을 장시간 점유하는 문제를 해결할 수 있다.
(6) DBMS
- DBMS란?
- 구조화된 데이터 조직인 데이터베이스(DB)를 관리하는 시스템. 대부분의 애플리케이션은 계산 중심보다는 데이터 중심적이기 때문에 학습할 필요가 있다.
- 트랜잭션, 격리 수준, 역정규화, 페이징 처리, 인덱스, 정규화, 트리거, 뷰, 샤딩, 스토어드 프로시저 등을 통해 DBMS를 활용할 수 있다.
(7) RestClient에 timeout과 retry 설정하는 방법
- 타임아웃을 설정해야하는 이유 : RestTemplate이나 WebClient를 사용할 때, 타임아웃을 명시적으로 설정하지 않으면 무제한으로 설정되어 서비스 속도에 영향을 주게 되며 최악의 경우 모든 스레드가 대기 상태에 빠져 다른 클라이언 요청에 응답할 수 있는 스레드가 남아있지 않게 된다.
- Connection Timeout : 설정한 시간까지 서버에 연결하는 시간이 오래 걸렸을 때 발생한다.
- Read Timeout : 클라이언트와 서버가 연결은 되었지만, 서버가 클라이언트의 요청을 정상적으로 처리하지 못하였을 때 발생하는 경우가 많다.
- spring boot의 기본 Http 클라이언트인 ClientHttpRequestFactorySettings와 JdkClientHttpRequestFactory 사용
private final RestClient restClient;
public MyService(RestClient.Builder builder) {
this.restClient = createRestClient(builder);
}
private RestClient createRestClient(RestClient.Builder builder) {
// timeout 시간 설정
Duration delayMillis = Duration.ofMillis(1000); // 1초
// RestClient 생성
ClientHttpRequestFactorySettings requestFactorySettings = ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(delayMillis)
.withReadTimeout(delayMillis);
JdkClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactories.get(JdkClientHttpRequestFactory.class, requestFactorySettings);
return builder.requestFactory(clientHttpRequestFactory).build();
}
2. 이번주 궁금증들
(1) AOP와 사용자 지정 애노테이션인 @LoginAuthenticated를 활용하여 Login해야 사용할 수 있는 메서드들 지정하는 방법은?
- 애노테이션 정의
package gyeongdan.util.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginAuthenticated {
}
- AOP 설정 by @Aspect
package gyeongdan.util.annotation;
import gyeongdan.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Aspect
@Component
@RequiredArgsConstructor
public class LoginAspect {
private final HttpServletRequest request;
private final JwtUtil jwtUtil;
@Before("@annotation(gyeongdan.util.annotation.LoginAuthenticated)")
public void authenticate() {
String token = jwtUtil.resolveToken(request);
if (token == null || !jwtUtil.validateToken(token)) {
throw new RuntimeException("유효하지 않은 토큰입니다.");
}
Long userId = jwtUtil.getUserId(token).orElseThrow(() -> new RuntimeException("사용자 ID를 찾을 수 없습니다."));
String username = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, Collections.singletonList(new SimpleGrantedAuthority(role))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
- Spring 설정 파일에 AOP 활성화
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// 다른 설정들
}
- 애노테이션을 특정 메서드에 적용시키기
@LoginAuthenticated
@GetMapping("/recommend")
public ResponseEntity<?> getUserTypeArticles() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Long userId = Long.valueOf(authentication.getName());
// 추천 알고리즘 결과를 List에 저장 후 반환
List<ArticleAllResponse> articles = recommendService.recommendArticleById(userId);
return ResponseEntity.ok(new CommonResponse<>(articles, "추천 기사 10개 가져오기 성공", true));
}
(2) lombok을 사용하여 @Slf4j의 logger을 사용하자 (soutv 대신)
(3) 컨트롤러의 파라미터와 반환값에 dto를 적극적으로 활용하자!! (가독성과 유지보수 측면에서 good)
(4) 설정 파일값을 사용하는 방법 (@Value 아님!!)
- API 키와 같은 것들을 설정 파일에 은밀하게 숨길 때 사용한다.
- ⭐️⭐️ @ConfiugartionProperties(prefix = ~) 사용
package gift.common.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "kakao.login")
public record KakaoProperties(
String getAuthCodeUri,
String getMessageToMeUri,
String clientId,
String redirectUri,
String grantType) {
}
- application.java
@EnableConfigurationProperties // 설정값 scan을 가능하게 함
@ConfigurationPropertiesScan // scan함
(5) var은 왜 사용하는 걸까?
- 초기화된 값으로부터 타입을 쉽게 추론할 수 있는 경우 사용
- var = url = "https://www.naver.com"; 과 같이 누가 봐도 String일 때 사용하듯
- 코드의 가독성을 해치지 않는 범위 내에서 사용
- 성능적으로는 크게 문제되지 않는다. var 또한 컴파일 단계에서 변수의 타입이 결정되기 때문이다.
- 단, 공부하는 단계에서는 매우 비추
(6) 큰 도메인 단위로 패키지를 나누자 !!!
ex) chatbot - controller, repository, domain (O)
chatbot - terms, introduce ... (X)
(7) 항상 @Entity의 소속을 잘 나타내자.
@Entity(name = "faq")
@Table(name = "faq", schema = "gyeongdan")
(8) ⭐️ record를 통해 json 형태의 응답에서 원하는 값만 추출할 수 있는 건 알겠어! 그런데 아래와 같이 json 안에 json이 있다면..?
{
"id":123456789,
"connected_at": "2022-04-11T01:45:28Z",
"kakao_account": {
// 프로필 또는 닉네임 동의항목 필요
"profile_nickname_needs_agreement ": false,
// 프로필 또는 프로필 사진 동의항목 필요
"profile_image_needs_agreement ": false,
"profile": {
// 프로필 또는 닉네임 동의항목 필요
"nickname": "홍길동",
// 프로필 또는 프로필 사진 동의항목 필요
"thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
"profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
"is_default_image":false,
"is_default_nickname": false
},
// 이름 동의항목 필요
- JsonNode 데이터타입과 readTree 메서드를 활용해보자
// KakaoMember
JsonNode jsonNode = objectMapper.readTree(response);
JsonNode kakaoAccount = jsonNode.get("kakao_account");
Long id = jsonNode.get("id").asLong();
String nickname = kakaoAccount.get("profile").get("nickname").asText();
(9) ObjectMapper 활용??
- Jackson 라이브러리 클래스
- Java 객체를 JSON으로 변환하거나 JSON을 Java 객체로 변환하는데 사용함.
- 객체를 JSON 문자열로 변환 : writeValueAsString 메서드 사용
- JSON 문자열을 객체로 변환 : readValue 메서드 사용
(10) MultiValueMap의 쓰임 : 하나의 키 값에 여러 값을 가질 수 있는 Map이다.
3. 이번주차 과제 수행과정
1. 카카오 로그인 구현
(1) 인가 코드 받기
- 인가 코드를 받는 이유는?
- 클라이언트가 어떤 것을 인가 받는지 알아야하기 때문이다.
- 기본 동작 : 카카오톡 로그인 -> 사용자가 동의항목에 동의 -> 인가 코드 발급
- API 명세서
- client_id : REST API 키
- redirect_uri : 인가 코드를 전달 받을 서비스 서버의 URI
- response_type : code로 고정함
- scope : 추가로 동의 받을 항목의 ID 목록을 지정 가능. 예를 들어 이메일, 성별, 톡 메시지 등등
GET https://kauth.kakao.com/oauth/authorize
client_id String 필수 O
redirect_uri String 필수 O
response_type String 필수 O
scope String 필수 X
(2) 토큰 받기
- 기본 동작 : 발급 받은 인가 코드를 토대로 토큰 발급 받기
- API 명세서
POST https://kauth.kakao.com/oauth/token
[Header]
Content-type : application/x-www-form-urlencoded;charset=utf-8 필수 O
[Body]
grant_type String 필수 O
client_id String 필수 O
redirect_uri String 필수 O
code String 필수 O
ex)
Request
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=${REST_API_KEY}" \
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
-d "code=${AUTHORIZE_CODE}"
Response
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"token_type":"bearer",
"access_token":"${ACCESS_TOKEN}",
"expires_in":43199,
"refresh_token":"${REFRESH_TOKEN}",
"refresh_token_expires_in":5184000,
"scope":"account_email profile"
}
(3) 직접적인 과제 수행
- ⭐️⭐️⭐️ 인가 코드 받고 바로 토큰 값 받기
// 1. 카카오 로그인을 통해 인가 코드를 받아오기
// sendRedirect 함수를 통하여 HTTP 상태 코드 302를 전송한다. 이때, 클라이언트는 해당 URL로 리다이렉트 된다.
// 이를 통해 Location: ${REDIRECT_URI}?code=${AUTHORIZE_CODE}로 Get 요청이 이루어진다.
@GetMapping()
public ResponseEntity<?> requestKakaoLoginScreen(HttpServletResponse response) throws IOException {
String url = kakaoProperties.getAuthCodeUri() +
"?client_id=" + kakaoProperties.clientId() +
"&redirect_uri=" + kakaoProperties.redirectUri() +
"&response_type=code";
response.sendRedirect(url);
return ResponseEntity.ok(new CommonResponse<>(null, "카카오 로그인 화면 요청 성공", true));
}
// 로그인 후 받은 인가 코드를 통해 access token을 발급 받기
// 이때, 토큰값과 사용자 정보를 DB에 저장하기
@GetMapping("/callback")
public ResponseEntity<?> getAccessToken(@RequestParam("code") String code) throws IOException {
// 2. 토큰 발급 받기
KakaoTokenResponse tokenResponse = kakaoService.getToken(code).getBody();
if (tokenResponse == null) {
return ResponseEntity.badRequest().body(new CommonResponse<>(null, "토큰 추출 실패", false));
}
// 3. 로그인한 사용자 정보 추출하기
KakaoUser kakaoUserInfo = kakaoService.getUserInfo(tokenResponse.accessToken());
if (kakaoUserInfo == null) {
return ResponseEntity.badRequest().body(new CommonResponse<>(null, "사용자 정보 추출 실패", false));
}
// 4. 토큰값과 사용자 정보를 DB에 저장하기
kakaoService.saveToken(tokenResponse, kakaoUserInfo);
return ResponseEntity.ok(new CommonResponse<>(tokenResponse, "토큰 추출 성공", true));
}
- 앞서 정리했던 ConfigurationProperties를 통해 KakaoProperties 레코드 만들기
- 앞서 정리했던 RestClient 사용하기
- 액세스 토큰이 유효한지 판단하는 방법 : 토큰값을 활용해 카카오 사용자 정보를 가져오고 해당 사용자 정보가 DB에 있는 사용자 정보랑 매칭되는지 확인하기. 만약 유효하지 못하다면 refreshToken으로 accessToken 재발급 받기
2. 주문하기
- 카카오톡 메시지 보내기의 나에게 보내기를 읽고 1. 카카오 로그인 구현과 같이 구현하였다.
4. 기타
- 카테캠 커리어 컨설팅 후기
- 백엔드 개발자로서 갖춰야하는 하드 스킬과 소프트 스킬의 우선순위는 무엇인가요?
- 초급자 단계
- 하드 스킬 : spring boot를 중점적으로 backend 로드맵을 따라 여러 기술들을 경험해보기. 단, 공부하며 항상 '왜?'라는 질문을 끊임 없이 되새기자.
- 소프트 스킬 : 의사소통, 표정관리, 적응력
- 중급자 단계
- 스케쥴 관리가 잘 되어야 한다. 주어진 업무가 언제쯤 끝날지 아는 능력!
- 고급자 단계
- 아키텍처, 공동 모듈 개발, 설계 능력이 필요하다.
- 초급자 단계
- 보통 사기업 개발자는 기능 개발과 유지보수를 중점적으로 한다는데 이와 별개로 다른 일들도 하는지 궁금합니다!
- SI 직군 : 개발자로서 첫 회사를 SI 직군으로 추천. 기능 개발이 중점적이다.
- SM 직군 : 유지보수, 운영 업무 등의 활동에 집중.
- 개발자 이후의 진로가 어떤 것들이 있는지 궁금합니다!
- SI 혹은 SM 직군에서 3~5년 정도 일한 뒤 자신과 맞는 비즈니스 도메인을 정한 뒤 고도화해야 한다. ex) 금융, 유통, 이커머스, 클라우드, 쇼핑몰 등