[게시판 프로젝트] 소셜로그인 기능 구현(2) - 카카오로 로그인 하기
2024. 8. 27. 02:47ㆍCS/프로젝트
회원 UserAccount 엔티티 수정
- 회원 도메인이 인증정보가 없는 상황에서도 회원 정보를 저장할 수 있는 방법을 마련해 줘야 함
- 소셜로그인 구현하기(1)에서 AuditingFields의 접근제어자를 protected로 열어줌
- UserAccount 생성자에 String createdBy 추가
- this.createdBy = createdBy;
- this.modifiedBy = createdBy;
- 수정하는 상황이 아니라, 최초로 생성하는 시점에 생성자와 수정자는 같기 때문에 createdBy
private UserAccount(String userId, String userPassword, String email, String nickname, String memo, String createdBy) {
this.userId = userId;
this.userPassword = userPassword;
this.email = email;
this.nickname = nickname;
this.memo = memo;
this.createdBy = createdBy;
this.modifiedBy = createdBy; //수정하는 상황이 아니라, 최초로 생성하는 시점에 생성자와 수정자는 같기 때문에 createdBy
}
- 모든 엔티티 메소드가 만들어질 때 호출되는 생성자 메서드 코드의 파리미터가 바뀜(String createdBy)가 추가됨
- 그럼 이 도메인을 쓰는 모든 클래스들의 코드를 변경해 주어야할까? NO!
- 생성자는 private으로 만들어서 접근이 안되고, 팩토리 메서드를 통해서 생성하게끔 코드를 사용하고 있기 때문에 코드를 변경 해 줄 필요 없음
public static UserAccount of(String userId, String userPassword, String email, String nickname, String memo) {
return UserAccount.of(userId, userPassword, email, nickname, memo, null); //null을 넣어줌
}
//인증 정보 없는데, 최초의 순간
public static UserAccount of(String userId, String userPassword, String email, String nickname, String memo, String createdBy) {
return new UserAccount(userId, userPassword, email, nickname, memo, createdBy);
}
BoardPrincipal : OAuth 인증 정보를 다룰 수 있도록 기능 추가
public record BoardPrincipal(
String username,
String password,
Collection<? extends GrantedAuthority> authorities,
String email,
String nickname,
String memo
) implements UserDetails {
- 우리는 BoardPrincipal이 UserDetails를 구현하도록 해서, 스프링 시큐리티가 인지할 수 있는 인증 정보를 담는 클래스 라는 사실을 전달할 수 있었음
기본 스프링 시큐리티 인증 정보뿐 아니라, OAuth2 인증 기술도 다룰 수 있게끔 만들어 주어야함
public record BoardPrincipal(
String username,
String password,
Collection<? extends GrantedAuthority> authorities,
String email,
String nickname,
String memo
Map<String, Object> oAuth2Attribute;
) implements UserDetails, OAuth2User {
생성자 대응 변경
- 기존의 oAuth2Attribute 없는 생성자 구조를 return of로 하여 변경
- 새로운 oAuth2Attribute를 추가한 생성자 구조 추가
public static BoardPrincipal of(String username, String password, String email, String nickname, String memo) {
Set<RoleType> roleTypes = Set.of(RoleType.USER);
return of(username,password,email,nickname,memo,Map.of());
}
public static BoardPrincipal of(String username, String password, String email, String nickname, String memo, Map<String, Object> oAuth2Attribute) {
Set<RoleType> roleTypes = Set.of(RoleType.USER);
return new BoardPrincipal(
username,
password,
roleTypes.stream()
.map(RoleType::getName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toUnmodifiableSet()),
email,
nickname,
memo,
oAuth2Attribute
);
}
OAuth2User 메서드 재정의
- OAuth2 인증 서비스마다 인증 결과로 내려주는 회원 정보나 각종 인증 정보들이 RESTful 하게 만들어져 있어서 JSON 형식으로 응답을 받는다고 하더라도 다 그 구조가 다름
- 따라서 어떤 구조든 key value 방식으로만 내려준다면 받아들일 수 있게끔 큰 범위로 뚫려 있는 것
//OAuth2 인증 정보
@Override public Map<String, Object> getAttributes() { return oAuth2Attribute;}
@Override public String getName() {return username;}
카카오 인증 정보 정의 - KakaoOAuth2Response
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
@SuppressWarnings("unchecked") // TODO: Map -> Object 변환 로직이 있어 제네릭 타입 캐스팅 문제를 무시한다. 더 좋은 방법이 있다면 고려할 수 있음.
public record KakaoOAuth2Response(
Long id,//회원번호
LocalDateTime connectedAt,//서비스에 연결 완료된 시각
Map<String, Object> properties,//사용자 프로퍼티 - JSON
KakaoAccount kakaoAccount//카카오 계정 정보 - KakaoAccount
) {
public record KakaoAccount(
Boolean profileNicknameNeedsAgreement,
Profile profile,
Boolean hasEmail,
Boolean emailNeedsAgreement,
Boolean isEmailValid,
Boolean isEmailVerified,
String email
) {
public record Profile(String nickname) {
public static Profile from(Map<String, Object> attributes) {
return new Profile(String.valueOf(attributes.get("nickname")));
}
}
public static KakaoAccount from(Map<String, Object> attributes) {
return new KakaoAccount(
Boolean.valueOf(String.valueOf(attributes.get("profile_nickname_needs_agreement"))),
Profile.from((Map<String, Object>) attributes.get("profile")),
Boolean.valueOf(String.valueOf(attributes.get("has_email"))),
Boolean.valueOf(String.valueOf(attributes.get("email_needs_agreement"))),
Boolean.valueOf(String.valueOf(attributes.get("is_email_valid"))),
Boolean.valueOf(String.valueOf(attributes.get("is_email_verified"))),
String.valueOf(attributes.get("email"))
);
}
public String nickname() { return this.profile().nickname(); }
}
public static KakaoOAuth2Response from(Map<String, Object> attributes) {
return new KakaoOAuth2Response(
Long.valueOf(String.valueOf(attributes.get("id"))),
LocalDateTime.parse(
String.valueOf(attributes.get("connected_at")),
DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.systemDefault())
),
(Map<String, Object>) attributes.get("properties"),
KakaoAccount.from((Map<String, Object>) attributes.get("kakao_account"))
);
}
public String email() { return this.kakaoAccount().email(); }
public String nickname() { return this.kakaoAccount().nickname(); }
}
- 이 코드는 카카오 OAuth2 인증 응답을 처리하기 위한 Java record 타입의 클래스를 정의한 것입니다.
- record는 Java 14에서 도입된 새로운 클래스 타입으로, 불변 객체를 쉽게 정의할 수 있습니다.
클래스 및 레코드 정의
@SuppressWarnings("unchecked")
- 제네릭 타입 캐스팅에 대한 경고를 무시하겠다는 어노테이션입니다.
- 이 코드에서는 Map<String, Object> 타입을 다른 구체적인 제네릭 타입으로 변환할 때 경고가 발생할 수 있기 때문에 사용되었습니다.
public record KakaoOAuth2Response(
Long id,
LocalDateTime connectedAt,
Map<String, Object> properties,
KakaoAccount kakaoAccount
) {
- KakaoOAuth2Response라는 레코드 타입을 정의합니다. 이 레코드는 다음의 필드를 가집니다:
- id: 카카오 회원 번호 (Long 타입)
- connectedAt: 서비스에 연결이 완료된 시간 (LocalDateTime 타입)
- properties: 사용자 프로퍼티를 담고 있는 맵 (Map<String, Object> 타입)
- kakaoAccount: 카카오 계정 정보를 담은 KakaoAccount 레코드
내부 레코드 KakaoAccount
public record KakaoAccount(
Boolean profileNicknameNeedsAgreement,
Profile profile,
Boolean hasEmail,
Boolean emailNeedsAgreement,
Boolean isEmailValid,
Boolean isEmailVerified,
String email
) {
- KakaoAccount라는 내부 레코드 타입을 정의합니다. 이 레코드는 카카오 계정 정보를 담습니다:
- profileNicknameNeedsAgreement: 닉네임 제공 동의 여부 (Boolean 타입)
- profile: 프로필 정보를 담은 Profile 레코드 (Profile 타입)
- hasEmail: 이메일 존재 여부 (Boolean 타입)
- emailNeedsAgreement: 이메일 제공 동의 여부 (Boolean 타입)
- isEmailValid: 이메일 유효 여부 (Boolean 타입)
- isEmailVerified: 이메일 인증 여부 (Boolean 타입)
- email: 이메일 주소 (String 타입)
public record Profile(String nickname) {
- Profile이라는 또 다른 내부 레코드 타입을 정의합니다. 이 레코드는 사용자의 닉네임을 담습니다:
- nickname: 닉네임 (String 타입)
public static Profile from(Map<String, Object> attributes) {
return new Profile(String.valueOf(attributes.get("nickname")));
}
- Profile 레코드의 정적 메서드 from은 주어진 Map<String, Object>에서 닉네임을 추출해 Profile 객체를 생성합니다.
- 이 때 Map에서 값을 가져올 때 String으로 변환하고 있습니다.
public static KakaoAccount from(Map<String, Object> attributes) {
return new KakaoAccount(
Boolean.valueOf(String.valueOf(attributes.get("profile_nickname_needs_agreement"))),
Profile.from((Map<String, Object>) attributes.get("profile")),
Boolean.valueOf(String.valueOf(attributes.get("has_email"))),
Boolean.valueOf(String.valueOf(attributes.get("email_needs_agreement"))),
Boolean.valueOf(String.valueOf(attributes.get("is_email_valid"))),
Boolean.valueOf(String.valueOf(attributes.get("is_email_verified"))),
String.valueOf(attributes.get("email"))
);
}
- KakaoAccount 레코드의 정적 메서드 from은 Map<String, Object>에서 각 필드를 추출해 KakaoAccount 객체를 생성합니다. 각 필드는 적절한 타입으로 변환됩니다.
public String nickname() { return this.profile().nickname(); }
- KakaoAccount 레코드에서 nickname 필드를 반환하는 편의 메서드입니다. 내부 Profile 객체에서 닉네임을 가져옵니다.
KakaoOAuth2Response의 정적 메서드와 편의 메서드
public static KakaoOAuth2Response from(Map<String, Object> attributes) {
return new KakaoOAuth2Response(
Long.valueOf(String.valueOf(attributes.get("id"))),
LocalDateTime.parse(
String.valueOf(attributes.get("connected_at")),
DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.systemDefault())
),
(Map<String, Object>) attributes.get("properties"),
KakaoAccount.from((Map<String, Object>) attributes.get("kakao_account"))
);
}
- KakaoOAuth2Response 레코드의 정적 메서드 from은 Map<String, Object>에서 값을 추출해 KakaoOAuth2Response 객체를 생성합니다.
- id는 Long 타입으로 변환합니다.
- connectedAt은 LocalDateTime으로 변환하며, ISO 8601 포맷을 기준으로 하고 시스템의 기본 시간대를 사용합니다.
- properties는 맵 타입으로 그대로 사용됩니다.
- kakaoAccount는 KakaoAccount 객체로 변환됩니다.
public String email() { return this.kakaoAccount().email(); }
public String nickname() { return this.kakaoAccount().nickname(); }
}
- KakaoOAuth2Response에서 email과 nickname 필드를 반환하는 편의 메서드입니다. 내부 KakaoAccount 객체의 값을 반환합니다.
요약
- 이 코드는 카카오 OAuth2 인증 응답을 객체로 변환하여 사용할 수 있도록 설계되었습니다.
- JSON 형태로 제공되는 응답을 Map<String, Object>로 받고, 이를 KakaoOAuth2Response 객체로 변환하여 필요한 정보를 쉽게 접근할 수 있도록 합니다.
- record를 사용해 불변 객체를 간결하게 정의했으며, 정적 팩토리 메서드(from)를 통해 JSON 데이터를 객체로 매핑하고 있습니다.
KakaoOAuth2ResponseTest
- 이 테스트 코드는 KakaoOAuth2Response 클래스가 JSON 형식의 인증 응답을 올바르게 파싱하여 우리가 의도하는 데이터 디자인으로 들어오는지
- 즉, KakaoOAuth2Response 객체로 변환하는지 확인하기 위해 작성되었습니다.
클래스와 테스트 설정
@DisplayName("DTO - Kakao OAuth 2.0 인증 응답 데이터 테스트")
class KakaoOAuth2ResponseTest {
- KakaoOAuth2ResponseTest라는 JUnit 테스트 클래스를 정의하고 있습니다.
private final ObjectMapper mapper = new ObjectMapper();
- ObjectMapper는 Jackson 라이브러리의 객체로, JSON 데이터를 Java 객체로 변환하거나 Java 객체를 JSON으로 변환할 때 사용됩니다. 여기서는 JSON 데이터를 Map으로 변환하는 데 사용됩니다.
개별 테스트 메서드
@DisplayName("인증 결과를 Map(deserialized json)으로 받으면, 카카오 인증 응답 객체로 변환한다.")
@Test
void givenMapFromJson_whenInstantiating_thenReturnsKakaoResponseObject() throws Exception {
- 이 메서드는 given-when-then 패턴을 사용하여 테스트를 구성합니다.
- @DisplayName은 테스트 메서드의 설명을 제공하며, @Test는 이 메서드가 테스트 메서드임을 나타냅니다.
- 이 메서드는 Exception을 던질 수 있습니다. 주로 ObjectMapper를 사용해 JSON을 파싱할 때 발생할 수 있는 예외를 처리하기 위함입니다.
Given
String serializedResponse = """
{
"id": 1234567890,
"connected_at": "2022-01-02T00:12:34Z",
"properties": {
"nickname": "홍길동"
},
"kakao_account": {
"profile_nickname_needs_agreement": false,
"profile": {
"nickname": "홍길동"
},
"has_email": true,
"email_needs_agreement": false,
"is_email_valid": true,
"is_email_verified": true,
"email": "test@gmail.com"
}
}
""";
Map<String, Object> attributes = mapper.readValue(serializedResponse, new TypeReference<>() {});
- serializedResponse는 JSON 형식으로 카카오 OAuth2 응답 데이터를 정의하고 있습니다. 이 JSON은 사용자의 ID, 연결 시간, 프로필 정보, 이메일 정보를 포함하고 있습니다.
- ObjectMapper의 readValue 메서드를 사용해 JSON 문자열을 Map<String, Object>로 디시리얼라이즈합니다. TypeReference<>는 타입 정보를 제공하기 위해 사용됩니다.
When
KakaoOAuth2Response result = KakaoOAuth2Response.from(attributes);
- KakaoOAuth2Response.from(attributes)를 호출하여, Map<String, Object> 형식의 데이터를 KakaoOAuth2Response 객체로 변환합니다. 여기서 attributes는 앞에서 디시리얼라이즈한 JSON 데이터를 포함하고 있습니다.
Then
assertThat(result)
.hasFieldOrPropertyWithValue("id", 1234567890L)
.hasFieldOrPropertyWithValue("connectedAt", ZonedDateTime.of(2022, 1, 2, 0, 12, 34, 0, ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime()
)
.hasFieldOrPropertyWithValue("kakaoAccount.profile.nickname", "홍길동")
.hasFieldOrPropertyWithValue("kakaoAccount.email", "test@gmail.com");
- assertThat(result)를 사용해 result 객체가 예상한 값을 가지는지 검증합니다. result 객체는 KakaoOAuth2Response 타입입니다.
- hasFieldOrPropertyWithValue 메서드를 사용해 필드 값이 예상한 값과 일치하는지 확인합니다:
- id 필드가 1234567890L인지 확인합니다.
- connectedAt 필드가 "2022-01-02T00:12:34Z"로부터 변환된 LocalDateTime과 일치하는지 확인합니다.
- kakaoAccount.profile.nickname 필드가 "홍길동"인지 확인합니다.
- kakaoAccount.email 필드가 "test@gmail.com"인지 확인합니다.
요약
- 이 테스트는 KakaoOAuth2Response.from() 메서드가 Map<String, Object>로부터 올바르게 데이터를 읽어와 KakaoOAuth2Response 객체를 생성하는지 확인합니다
- 특히, id, connectedAt, profile.nickname, 그리고 email 필드가 예상한 값과 일치하는지 검증합니다.
- 이 테스트는 JSON을 Java 객체로 변환하는 로직이 올바르게 작동하는지 확인하는 데 매우 유용합니다
'CS > 프로젝트' 카테고리의 다른 글
[게시판 프로젝트] 깃헙 릴리즈하기 (0) | 2024.09.04 |
---|---|
[게시판 프로젝트] 소셜 로그인 기능 구현(3) - 카카오로 로그인 하기 (0) | 2024.08.27 |
[게시판 프로젝트] 소셜로그인 기능 구현(1) - 카카오로 로그인 하기 - 카카오 API 사용준비/의존성/프로퍼티 설정 (0) | 2024.08.27 |
[게시판 프로젝트] 게시글 댓글 구현 - ArticleCommentController 코드 뜯어보며, 댓글 기능 프로세스 이해하기 (0) | 2024.08.26 |
[게시판 프로젝트] 게시판 검색 구현 (0) | 2024.08.26 |