QueryDsl 왜 쓰나요?

JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다.
간단한 로직은 큰 문제가 없으나, 복잡한 로직의 경우 쿼리 문자열이 상당히 길어집니다.
JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생하게 됩니다.

이러한 문제를 어느정도 해소하는데 기여하는 프레임워크가 바로 QueryDsl 입니다.
QueryDsl은 정적 타입을 이용해서 Query를 생성해주는 프레임워크입니다.


QueryDsl의 장점과 단점은 무엇인가요?


QueryDsl 장점

1) 문자가 아닌 코드로 쿼리를 작성함으로써 컴파일 시점에 문법 오류를 쉽게 확인할 수 있습니다.

2) IDE의 자동완성으로 도움을 받을 수 있습니다.

3) 동적인 쿼리 작성이 편리합니다.

4) 쿼리 작성시 제약 조건 등을 메서드 추출을 통해 간편하고 재사용할 수 있습니다.


QueryDsl 단점

1) 번거로운 Gradle 설정 및 사용법을 배워야 합니다.



QueryDsl 예제 코드

  • Springboot 2.7.10, java 11, gradle 8.4 기반으로 작성한 예제 코드입니다.
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.14'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.oauth2.resource.server'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '11'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.14'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    // queryDsl 설정
    implementation "com.querydsl:querydsl-jpa"
    implementation "com.querydsl:querydsl-core"
    implementation "com.querydsl:querydsl-collections"
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
    delete file(generated)
}

//tasks.named('test') {
//    useJUnitPlatform()
//}
사용자 Entity
package com.cafe.be.api.common.entity;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.sql.Timestamp;
import java.time.LocalDateTime;

@MappedSuperclass
public class BaseEntity {

    // 생성일
    @CreationTimestamp
    private LocalDateTime createDt;

    // 수정일
    @UpdateTimestamp
    private LocalDateTime modifyDt;

}

package com.cafe.be.api.userservice.entity;

import com.cafe.be.api.common.entity.BaseEntity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Users extends BaseEntity {

    // 사용자 번호
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userNo;

    // 이메일
    private String email;

    // 비밀번호
    private String pw;

    // 닉네임
    private String nick;

    // 사용자 상태
    private String status;

    // 권한
    private String role;

    // 생년월일
    private String birth;

    // 이용약관 동의
    private String agree1;

    // 개인정보 수집 및 이용 동의
    private String agree2;

    // 이벤트, 프로모션, 메일, sms 수신
    private String agree3;

}
사용자 Repository
package com.cafe.be.api.userservice.repo;

import com.cafe.be.api.userservice.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UsersRepository extends JpaRepository<Users, Long> {
}
QueryDsl Config
package com.cafe.be.api.common.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;

@Configuration
@RequiredArgsConstructor
public class QuerydslConfig{

    private final EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(em);
    }
}
사용자 QueryDsl
package com.cafe.be.api.userservice.querydsl;

import com.cafe.be.api.userservice.dto.QUsersResponse;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import static com.cafe.be.api.userservice.entity.QUsers.users;

@Repository
@RequiredArgsConstructor
public class UserQueryDsl {

    private final JPAQueryFactory queryFactory;

    public UsersResponse findByUserNo(Long userNo){
        UsersResponse usersResponse = queryFactory
                .select(
                        new QUsersResponse(
                                users.userNo,
                                users.email,
                                users.pw,
                                users.nick,
                                users.status,
                                users.role,
                                users.birth,
                                users.agree1,
                                users.agree2,
                                users.agree3
                        )
                ).from(users)
                .where(
                        users.userNo.eq(userNo)
                ).fetchFirst();

        return usersResponse;
    }

}
사용자 DTO
package com.cafe.be.api.userservice.dto;

import com.cafe.be.api.userservice.entity.Users;
import com.querydsl.core.annotations.QueryProjection;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@Builder
public class UsersResponse {

    private Long userNo;

    // 이메일
    private String email;

    // 비밀번호
    private String pw;

    // 닉네임
    private String nick;

    // 사용자 상태
    private String status;

    // 권한
    private String role;

    // 생년월일
    private String birth;

    // 이용약관 동의
    private String agree1;

    // 개인정보 수집 및 이용 동의
    private String agree2;

    // 이벤트, 프로모션, 메일, sms 수신
    private String agree3;

    @QueryProjection
    public UsersResponse(Long userNo, String email, String pw, String nick, String status, String role, String birth, String agree1, String agree2, String agree3) {
        this.userNo = userNo;
        this.email = email;
        this.pw = pw;
        this.nick = nick;
        this.status = status;
        this.role = role;
        this.birth = birth;
        this.agree1 = agree1;
        this.agree2 = agree2;
        this.agree3 = agree3;
    }


    public static UsersResponse UsersToUsersResponse(Users u){
        UsersResponse usersResponse = UsersResponse.builder()
                .userNo(u.getUserNo())
                .email(u.getEmail())
                .pw(u.getPw())
                .nick(u.getNick())
                .status(u.getStatus())
                .role(u.getRole())
                .birth(u.getBirth())
                .agree1(u.getAgree1())
                .agree2(u.getAgree2())
                .agree3(u.getAgree3())
                .build();

        return usersResponse;

    }

}
사용자 Controller
package com.cafe.be.api.userservice.controller;

import com.cafe.be.api.common.dto.CommonResponse;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.cafe.be.api.userservice.entity.Users;
import com.cafe.be.api.userservice.repo.UsersRepository;
import com.cafe.be.api.userservice.service.UsersService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UsersController {

    private final UsersService usersService;

    @GetMapping("/{userNo}")
    public CommonResponse<UsersResponse> detail(@PathVariable("userNo") Long userNo){

        return usersService.detail(userNo);

    }

}
사용자 Service
package com.cafe.be.api.userservice.service;

import com.cafe.be.api.common.dto.CommonResponse;
import com.cafe.be.api.common.enums.ResponseCode;
import com.cafe.be.api.common.exception.ApiException;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.cafe.be.api.userservice.entity.Users;
import com.cafe.be.api.userservice.querydsl.UserQueryDsl;
import com.cafe.be.api.userservice.repo.UsersRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class UsersService {

    private final UsersRepository usersRepository;

    private final UserQueryDsl userQueryDsl;

    public CommonResponse<UsersResponse> detail(Long userNo) {

        log.debug("[UsersService] [detail] request >>> {}", userNo);

        CommonResponse<UsersResponse> result = new CommonResponse<>();

        try{
//            Users users = usersRepository.findById(userNo)
//                    .orElseThrow(() -> new ApiException(ResponseCode.USER_NOT_FOUND));
//
//            UsersResponse usersResponse = UsersResponse.UsersToUsersResponse(users);


            UsersResponse usersResponse = userQueryDsl.findByUserNo(userNo);

            result.setResult(usersResponse);
            result.setCode(ResponseCode.SUCCESS.getCode());
            result.setMsg(ResponseCode.SUCCESS.getMsg());

        }catch (ApiException e){
            result.setCode(e.getCode());
            result.setMsg(e.getMsg());
        }

        return result;
    }
}
반응형

'개발 > JPA' 카테고리의 다른 글

JPA-OSIV(Open Session In View)  (0) 2023.10.14
[JPA] could not initialize proxy - no Session  (0) 2023.09.10
[JPA] GeneratedValue  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10

OAuth2란?

  • OAuth2는 사용자가 하나의 서비스(ex: 웹사이트, Application)에 로그인할 때 다른 서비스의 자격증명을 사용하여 인증할 수 있게 해주는 인증 프로토콜입니다.

ex)
새로운 웹사이트에 가입하려고 할 때, google, naver 로그인을 보셨을 겁니다.
이 경우, 해당 웹사이트는 사용자의 google 또는 naver 계정을 통해 사용자를 인증합니다.
사용자는 별도의 새로운 계정을 만들 필요 없이 기존의 소셜 미디어 계정을 사용하여 쉽게 로그인 할 수 있습니다.
이 과정에서 OAuth2 프로토콜이 사용되며, 사용자의 로그인 정보는 안전하게 보호됩니다.


OAuth2 구조

OAuth2 의 플로우는 위 아래 그림과 같습니다.

기본적으로는

  1. Client -> OAuth Authentication Server 인증코드 받기
  2. OAuth Authentication Server 에서 받은 인증코드를 기반으로 Client에서 OAuth Authentication Server로 token 발급
  3. OAuth Authentication Server 에서 받은 token 기반으로 OAuth Resource Server에서 Resource 할당


OAuth2 말하는 역할 및 주요 용어는 어떤게 있나요?

1) 역할

이름 설명
Resource Owner 리소스(자원) 소유자입니다.
본인의 정보에 접근할 수 있는 자격을 승인하는 주체입니다.
예를 들어보겠습니다.
서비스를 이용하기 위해 해당 서비스에서 제공하는 Google 간편 로그인으로 로그인을 한 경우,
해당 Google 아이디를 가지고 있는 본인이 Resource Owner가 되는 겁니다.
Client Resource Owner의 리소스(자원)를 사용하고자 접근 요청을 하는 서비스(어플리케이션) 입니다.
Resource Server Resource Owner의 정보가 저장되어 있는 서버입니다.
위 예시에서 Google 로그인을 하는 본인이 Resource Owner라고 했었습니다.
이 때, Google이 Resource Server가 되는 겁니다.
Autehorization Server Resource Server에서 리소스를 가져오기 위한 token(권한)을 할당해주는 권한 서버입니다.
인증 / 인가를 수행하는 서버로써 Client의 접근 자격을 확인하고 Access Token을 발급하여 권한을 부여하는 역할을 수행합니다.

2) 주요 용어

이름 설명
Authentication(인증) 인증, 접근 자격이 있는지 검증하는 단계입니다.
Authorization(인가) 자원에 접근할 권한을 부여하고 리소스 접근 권한이 담긴 Access Token을 제공합니다.
Access Token Resource Server 에서 Resource Owner 의 리소스(자원)를 획득 할 때 사용되는 만료기간이 있는 Token 입니다.
Refresh Token Access Token 만료시 이를 재발급 받기위한 용도로 사용하는 Token 입니다.



토큰(Token) 발급 방식

  • 아래에서 사용하는 URL은 기본 oauth2.0 코드 발급 URL 입니다.
  1. code (code를 발급 받고 code로 access token 발급)

1) Client -> Authentication Server 로 code를 발행 합니다.

2) 1번에서 발급받은 code를 기반으로 Client -> Authentication Server로 access token을 발급 받습니다.

  • 발급 받은 code를 가지고 token을 발행 합니다.
  • URL: http://localhost:8080/oauth2/token
  • Parameter: grant_type=code, authorization_code=발급받은 코드, redirect_uri=코드 전달받은 URL
  1. password(id, pw로 access token 발급)
  • id, pw로 token을 바로 발급 받을 수 있습니다.
  • URL: http://localhost:8080/oauth2/token
  • Parameter: grant_type=password, username=아이디, password=비밀번호, scope=read
  1. client_credentials(바로 access token 발행)
  • header의 authorization에 clientId, secret key만 등록된 정보를 기반으로 access token을 발급해 주며, 별다른 인증이 필요 없는 신뢰도가 높은 Client에게만 해당 방식을 허용하도록 합니다.
  • 해당 방식은 별다른 인증을 요하지 않기에 refresh_token은 따로 발행해주지 않습니다.
  • URL: http://localhost:8080/oauth2/token
  • Parameter: grant_type=client_credentials, scope=read
  1. refresh_token
  • refresh token으로 access token 발행합니다.
  • 1, 2번 방식으로 access_token 발행과 동시에 refresh_token 도 같이 발행해줍니다.
  • URL: http://localhost:8080/oauth2/token
  • Parameter: grant_type=refresh_token, refresh_token=이전에 발급받은 refresh token, scope=read



OAuth2 예시 코드

  • Springboot 2.7.10, java 11, gradle 8.4 환경에서 구현하였습니다.
  • 저는 OAuth2의 Resource Server와 Authentication Server를 합쳐서 구현하였습니다.
  • 단순 FrontEnd와 BackEnd 간의 통신에서 사용하기 위하여 구현하였기에 client_credentials 기반으로 구현하였습니다.



build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.14'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.oauth2.resource.server'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '11'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.security:spring-security-oauth2-resource-server:5.7.11'
    implementation 'org.springframework.security:spring-security-oauth2-jose:5.7.11'

    implementation 'org.springframework.security:spring-security-oauth2-authorization-server:0.3.1'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.14'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

}

security 설정

package com.cafe.be.security.config;

import com.cafe.be.security.handler.CustomAccessDeniedHandler;
import com.cafe.be.security.handler.CustomAuthenticationEntryPoint;
import com.cafe.be.oauth.handler.JwtAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@RequiredArgsConstructor
public class ResourceServerConfig{

    private final CustomAccessDeniedHandler customAccessDeniedHandler;

    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        http
            .headers().frameOptions().disable()
            .and()
                .csrf().ignoringAntMatchers("/h2-console/**").ignoringAntMatchers("/oauth2/token")
            .and()
                .authorizeRequests()
                    .mvcMatchers("/h2-console/**").permitAll()
                    .mvcMatchers("/oauth2/token").permitAll()
                .anyRequest().authenticated()
            .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .and()
                .exceptionHandling()
                .authenticationEntryPoint(customAuthenticationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler)
            .and()
                .oauth2ResourceServer()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .jwt();

        return http.build();
    }

}

OAuth2 설정

package com.cafe.be.oauth.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;

@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class) // OAuth2 인증 서버에 필요한 기본 설정을 가져옵니다.
public class AuthorizationServerConfig {

    @Value("${oauth.issuer.url}")
    private String issuerUrl;

    @Bean
    public TokenSettings tokenSettings() {
        return TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(10)) // 액세스 토큰 유효 기간을 10분으로 설정
                .build();
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception{
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.build();
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
        RSAKey rsaKey = generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);

        return ((jwkSelector, context) -> jwkSelector.select(jwkSet));
    }

    private RSAKey generateRsa() throws NoSuchAlgorithmException {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    private static KeyPair generateRsaKey() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        return keyPairGenerator.generateKeyPair();
    }

    @Bean
    public ProviderSettings providerSettings(){
        return ProviderSettings.builder()
                .issuer(issuerUrl)
                .build();
    }

}

OAuth2 관련 Entity

package com.cafe.be.oauth.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "oauth_client")
@Getter
@AllArgsConstructor
@Builder
public class OAuthClient {

    @Id
    private String clientId;

    private String clientSecret;

    private String scopes;

    private String grantType;

    protected OAuthClient() {}

}

OAuth2 관련 Enums

package com.cafe.be.oauth.enums;

public enum OAuthScopes {

    READ("cafe.read");

    private String scopeName;

    OAuthScopes(String scopeName) {
        this.scopeName = scopeName;
    }
}

OAuth2 관련 Repository

package com.cafe.be.oauth.repo;

import com.cafe.be.oauth.entity.OAuthClient;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.TokenSettings;
import org.springframework.stereotype.Repository;

import java.time.Duration;
import java.util.UUID;

@Repository
@RequiredArgsConstructor
public class CustomRegisteredClientRepository implements RegisteredClientRepository {

    private final OAuthClientRepository oauthClientRepository;

    @Override
    public void save(RegisteredClient registeredClient) {
        // 클라이언트 정보를 데이터베이스에 저장하는 로직을 구현합니다.
        OAuthClient oauthClient = OAuthClient.builder()
                .clientId(registeredClient.getClientId())
                .clientSecret(registeredClient.getClientSecret())
                .scopes(String.valueOf(registeredClient.getScopes()))
                .grantType(registeredClient.getAuthorizationGrantTypes().toString())
                .build();

        oauthClientRepository.save(oauthClient);
    }

    @Override
    public RegisteredClient findById(String id) {
        return oauthClientRepository.findById(id)
                .map(this::toRegisteredClient)
                .orElse(null);
    }

    @Override
    public RegisteredClient findByClientId(String clientId) {
        return oauthClientRepository.findByClientId(clientId)
                .map(this::toRegisteredClient)
                .orElse(null);
    }

    private RegisteredClient toRegisteredClient(OAuthClient oauthClient) {
        // OAuthClient 엔티티를 RegisteredClient 객체로 변환하는 로직
//        String grantType = oauthClient.getGrantType();
//
//        Set<AuthorizationGrantType> grantTypeSet = new HashSet<AuthorizationGrantType>();
//        if(grantType != null && grantType.equals("")){
//            String[] grantTypeSplit = grantType.split(",");
//
//            for (String type : grantTypeSplit) {
//                grantTypeSet.add(new AuthorizationGrantType(type));
//            }
//        }

        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId(oauthClient.getClientId())
                .clientSecret(oauthClient.getClientSecret())
                .scope(oauthClient.getScopes())
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(new AuthorizationGrantType(oauthClient.getGrantType()))
                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(1)).build())
//                .authorizationGrantTypes((Consumer<Set<AuthorizationGrantType>>) grantTypeSet)
                .build();

        return registeredClient;
    }
}

package com.cafe.be.oauth.repo;


import com.cafe.be.oauth.entity.OAuthClient;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface OAuthClientRepository extends JpaRepository<OAuthClient, String> {

    Optional<OAuthClient> findByClientId(String clientId);

}
반응형

'개발 > Spring' 카테고리의 다른 글

AOP란? AOP 내용, AOP 예제  (0) 2024.01.11
응집도와 결합도란?  (1) 2023.12.26
[Querydsl] DTO 조회 방법  (0) 2023.12.17
DTO <-> Entity 변환 이유!  (0) 2023.12.14

프로젝트에서 코드를 구현할 때 Entity를 그대로 사용하지 말고 DTO를 사용하라는 말을 들어본적이 있을겁니다.

그런데 왜?? 라고 생각해보신적이 있으신가요?

코드를 구현함에 있어 "그냥 대중적으로 사용하니까 그렇게 사용하는거지" or "일단 복붙"이라고 생각하시면 단순 코딩하는 로봇과 다를빠 없지 않을까요?

하물며.. AI가 판을 치는 세상에 단순 로봇이라니...


오늘 간단하게 DTO와 Entity가 무엇인지, 왜 DTO <-> Entity를 분리해서 쓰고 변환하는지에 대해 내용을 공유하고자 합니다.


DTO? Entity?

1. DTO(Data Transfer Object)란?

  • 클라이언트와 서버 간 데이터 전송을 목적으로 설계된 객체

2. Entity란?

  • 데이터베이스에 저장되는 데이터 객체로, 데이터베이스와 직접적으로 연결되는 객체

왜 분리해서 쓸까???

1. View와 Model 분리

  • DTO는 View와 Controller 간의 인터페이스 역할을 하며, Entity는 Model의 역할을 합니다.

2. 불필요한 데이터 노출 방지

  • Entity를 이용하여 Controller <-> View 구간에서 데이터를 주고 받는다면 해당 Entity(DB) 구조가 노출되기 때문에 필요한 정보값만 DTO로 정보를 노출시키도록 하는 것이 좋습니다.

3. 순환 참조 예방

  • DTO는 엔티티 간의 양방향 참조가 포함되지 않은 간단한 구조를 가지며, 필요한 정보들만 노출될 수 있습니다.
  • Controller <-> View 구간에서 Entity를 사용하여 정보를 전달하는 경우, @ManyToOne, @OneToMany 등의 순환참조들이 연결되어 있는 정보들에서 N+1 등 원하지 않는 문제가 발생할 수 있습니다.

4. Validation 코드와 모델링 코드 분리

  • validation 코드 : @NotNull, @NotEmpty, @NotBlank ...

  • 모델링 코드 : @Column, @JoinColumn, @ManyToOne, @OneToOne, @CasCade ...

  • Entity는 DB의 테이블과 매칭되는 모델링 코드만 구현해야 가독성을 높일 수 있습니다.

  • 각 필요한 데이터만 전달하고 각 데이터에 맞는 Validation 을 추가하면 Entity 클래스의 모델링에 집중할 수 있습니다.

DTO, Entity 사용 방법

구조

  • DTO는 값의 전달의 역할로써 Controller <-> View 구간, Controller <-> Service 구간에서 주로 이루어지며 Entity는 Service <-> Repository 구간에서 사용합니다.
  • Controller <-> Service, Controller <-> View 구간에서 DTO를 사용하는 이유는 해당 구간에서는 단순 값을 전달만 하면 되지만 Service <-> Repository 구간에서 Entity를 사용하는 이유는 Repository에서 DTO를 Entity로 변환하는 작업이 발생하는 경우, 기존 Repository의 역할에서 벗어나는 행위이기 때문에 Service에서 DTO를 Service로 변환 후 사용합니다.
반응형

'개발 > Spring' 카테고리의 다른 글

AOP란? AOP 내용, AOP 예제  (0) 2024.01.11
응집도와 결합도란?  (1) 2023.12.26
[Querydsl] DTO 조회 방법  (0) 2023.12.17
OAuth2란? OAuth2 예시  (1) 2023.12.15

+ Recent posts