AOP란?

  • Spring에서 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 프로그래밍에서 반복되는 코드를 모듈화하는 프로그래밍 패러다임입니다.
    이는 객체 지향 프로그래밍(OOP)을 보완하는 방식으로, 공통의 관심사(Cross-cutting Concerns)를 한 곳에 모아 관리합니다.
    AOP는 주로 로깅, 보안, 트랜잭션 관리와 같은 기능에서 사용됩니다.

  • AOP의 핵심 개념은 'Aspect'로 여러 객체에 공통으로 적용되는 기능을 의미합니다.
    예를 들어, 로깅은 애플리케이션의 많은 부분에서 필요하지만 각각의 객체나 메소드에 로깅 코드를 직접 삽입하는 것은 비효율적입니다.
    AOP를 사용하면, 공통 기능을 별도의 Aspect로 정의하고, 필요한 객체나 메소드에 독립적으로 적용할 수 있습니다.

AOP 개념 용어

  1. Aspect(관점)
  • AOP의 핵심 요소로, 흩어진 관심사를 모듈화한 것입니다.
  • 예를 들어 로깅, 트랜잭션 관리 또는 보안과 같은 기능을 Aspect로 정의할 수 있습니다.
  1. Join Point
  • Advice가 적용될 수 있는 위치, 예를 들어 메소드 실행, 객체 생성 등 프로그램 실행 중 특정 지점을 의미합니다.
  1. Advice
  • 실제로 Aspect가 어떤 일을 수행되는지 작업(로직)을 정의한 것입니다.
  • @Before: Join Point 전에 실행됩니다.
  • @After Returning: Join Point가 정상적으로 완료된 후에 실행됩니다.
  • @After Throwing: 메소드 실행 중 예외가 발생했을 때 실행됩니다.
  • @After: Joint Point가 실행된 후, 예외 발생 여부에 상관없이 실행됩니다.
  • @Arround: Joint Point 전후로 실행되며, 메소드 실행을 감싸는 형태입니다.
  1. Pointcut
  • Join Points 중에서 실제로 Advice가 적용될 위치를 선별하는 표현식입니다.
  • Pointcut을 통해 특정 메소드나 클래스의 메소드들에 대한 Advice 적용 여부를 정의할 수 있습니다.

@Pointcut

  • @Pointcut 은 Advice가 적용될 메소드나 클래스의 집합을 정의하는 데 사용되는 어노테이션입니다.
  • 쉽게 말해, @Pointcut은 어떤 메소드에 Advice를 적용할지 결정하는 "필터"와 같은 역할을 합니다.

@Pointcut을 사용하지 않은 경우

@Component
@Aspect
@Slf4j
public class MyAspect{
    // Before Advice: com.example 패키지 내 모든 메소드 실행 전에 적용
    @Before("execution(* com.example..*(..))")
    public void beforeAdvice(){
        log.info("Before method execution");
    }

    // After Advice: com.example 패키지 내 모든 메소드 실행 후에 적용
    @After("execution(* com.example..*(..))")
    public void afterAdvice(){
        log.info("After method execution");
    }
}

@Pointcut을 사용한 경우

@Component
@Aspect
@Slf4j
public class MyAspect{
    // Pointcut 정의: com.eample 패키지 내 모든 메소드
    @Pointcut("exectuion(* com.example..*(..))")
    public void myPointcut() {}

    // Before Advice: myPointcut에 정의된 메소드 실행 전에 적용
    @Before("myPointcut()")
    public void beforeAdvice(){
        log.info("Before method execution");
    }

    // After Advice: myPointcut에 정의된 메소드 실행 후에 적용
    @After("myPointcut()")
    public void afterAdvice(){
        log.info("After method execution");
    }
}

@Pointcut을 사용한 경우 이점

  1. 재사용성
  • @Pointcut을 사용하는 경우, @Pointcut을 한 곳에서 정의하고 여러 Advice에서 재사용 할 수 있어 코드의 중복을 줄일 수 있습니다.
  1. 유지보수
  • @Pointcut 을 사용하면, 추후 Pointcut을 변경할 때 한 곳에서만 수정하면 됩니다.
  1. 가독성
  • @Pointcut 을 사용하면, Pointcut의 목적을 이름으로 명확히 할 수 있어 가독성이 좋아집니다.

@Pointcut 사용 방법

execution

execution([수식어][리턴타입][클래스이름][이름]([파라미터])

  • 수식어: public, private 등 수식어를 명시합니다. (생략 가능)
  • 리턴타입: 리턴 타입을 명시합니다.
  • 클래스이름 및 이름: 클래스이름과 메서드 이름을 명시합니다. (클래스 이름은 풀 패키지명으로 명시해야 합니다. 생략 가능)
  • 파라미터: 메서드의 파라미터를 명시합니다.
  • "*": 모든 값을 표현합니다.
  • "..": 0개 이상을 의미합니다.

ex)
execution(public Integer com.example.aop..(*))
-> com.example.aop 패키지의 하위 클래스 중 public 접근자면서 반환타입이 Integer, 한개의 파라미터를 가지는 메소드

execution(com.example...get*(..))
-> com.example 패키지 및 하위 패키지의 클래스 중 이름이 get으로 시작하며 파라미터 수나 타입에 관계없이 모든 메서드

execution(com.example.aop..Service.*(..))
-> com.example.aop 패키지 및 하위 패키지에 속해있고 이름이 Service로 끝나는 파라미터 수나 타입에 관계없이 모든 메서드

execution(com.example.aop.BoardService.(.))
-> com.example.aop 패키지의 BoardService 클래스의 파라미터 수나 타입에 관계없이 모든 메서드

execution(some(,))
-> 메서드 이름이 some으로 시작하고 파라미터가 2개인 모든 메서드

within 명시자

ex)
within(com.example.aop.SomeService)
-> com.example.aop.SomeService 인터페이스의 모든 메서드

within(com.example.aop.*)
-> com.example.aop 패키지의 모든 메서드

within(com.example.aop..*)
-> com.example.aop 패키지 및 하위 패키지의 모든 메서드

bean 명시자

ex)
bean(someBean)
-> 이름이 someBean인 빈의 모든 메서드

bean(some*)
-> 빈의 이름이 some으로 시작하는 빈의 모든 메서드



Advice 사용 방법

  1. @Around
  • 핵심 메서드와 공통 메서드의 실행 시점을 자유롭게 설정할 수 있는
  • 어드바이스를 활용하면 메소드의 실행을 둘러싸 여러 가지 추가 기능을 구현할 수 있으며, 비즈니스 로직에 영향을 주지 않으면서 중요한 관심사를 관리할 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void monitor() { }

    // Around 어드바이스 정의
    @Around("monitor()")
    public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            // 대상 메소드 실행
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long elapsedTime = System.currentTimeMillis() - startTime;
            System.out.println(joinPoint.getSignature().getName() + " 실행 시간: " + elapsedTime + "ms");
        }
    }
}
  1. @AfterThrowing
  • 메소드 실행 중 예외가 발생한 후에 실행됩니다.
  • 발생한 예외에 대한 정보에 접근할 수 있습니다.
  • 발생한 예외에 따라 적절한 처리 로직을 수행할 수 있습니다.
  • 예외 상황에 대한 로그 기록, 리소스 정리, 사용자에게 오류 정보 전달 등 다양한 예외 처리 로직을 구현하는 데 유용하게 사용됩니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ErrorHandlingAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }

    // AfterThrowing 어드바이스 정의
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logAfterThrowingAllMethods(Exception ex) throws Throwable {
        System.out.println("Exception caught in AfterThrowing advice: " + ex.getMessage());
    }
}
  1. @AfterReturning
  • 메소드가 성공적으로 실행되고 결과를 반환한 후에 실행됩니다.
  • 대상 메소드의 반환값에 접근하여 처리할 수 있습니다.
  • 메소드 실행의 성공 여부를 로깅하는데 사용될 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }

    // AfterReturning 어드바이스 정의
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logAfterReturning(Object result) {
        System.out.println("Method returned value is : " + result);
    }
}
  1. @Before
  • 대상 메소드 실행 전에 특정 로직을 수행합니다.
  • 메소드가 실행되기 전 필요한 파라미터 검증이나 초기 설정을 수행할 수 있습니다.
  • 메소드 실행 전 정보를 로그로 남길 수 있습니다.
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 포인트컷 정의: Service 레이어의 모든 메소드
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() { }

    // Before 어드바이스 정의
    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before executing method: " + joinPoint.getSignature().getName());
    }
}
반응형

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

응집도와 결합도란?  (1) 2023.12.26
[Querydsl] DTO 조회 방법  (0) 2023.12.17
OAuth2란? OAuth2 예시  (1) 2023.12.15
DTO <-> Entity 변환 이유!  (0) 2023.12.14

응집도(Cohesion)란?

  • 응집도는 하나의 모듈이나 클래스 내부의 기능들이 얼마나 밀접하게 연관되어 있는지를 나타냅니다.
  • 즉, 하나의 모듈이 단일 기능이나 목적을 가지고 있는 정도를 말합니다.
  • 응집도가 높다는 것은 모듈 내의 모든 요소가 서로 긴밀하게 연결되어 있다는 것을 의미하며, 유지보수와 이해하기 쉬운 코드를 만드는데 중요한 역할을 합니다.

응집도 예시

  1. 높은 응집도
  • '계산기' 클래스에는 수학적 계산과 관련된 메소드들만 포함되어 있습니다.
  • '더하기', '빼기', '곱하기', '나누기' 등의 기능이 계산기 클래스에 포함됩니다.
  • '더하기', '빼기', '곱하기', '나누기' 각 메소드들은 계산기의 기능에 밀접하게 연관되어 있는 기능들로 응집도가 높습니다.
  1. 낮은 응집도
  • '도구상자' 클래스에는 파일을 읽는 메소드, 그래픽을 그리는 메소드, 데이터베이스 쿼리를 실행하는 메소드 등 서로 다른 기능들이 있습니다.
  • 이런식으로 하나의 기능을 특정 짓는 것이 아닌 관련없는 여러 기능들을 하나의 클래스에 선언할 때 응집도가 낮다라고 합니다.

결합도(Coupling)란?

  • 결합도는 서로 다른 모듈 간의 '의존성' 정도를 나타냅니다.
  • 모듈 간의 결합도가 낮다는 것은 각 모듈이 서로 독립적으로 기능한다는 것을 의미하며, 유지보수성과 확장성에 긍정적인 영향을 미칩니다.

결합도 예시

  1. 낮은 결합도
  • '사용자 인터페이스' 모듈과 '데이터 처리' 모듈이 서로 독립적으로 작동합니다.
  • 이는 하나의 모듈을 수정하더라도 다른 모듈에 영향을 주지 않는다는 것을 의미합니다.
  1. 높은 결합도
  • '주문 처리' 모듈이 '재고 관리' 모듈에 강하게 의존하고 있습니다.
  • 이 경우, '재고 관리' 모듈에 변화가 생기면 '주문 처리' 모듈도 이에 맞춰 변경해야 합니다.

응집도와 결합도의 균형

  • 효과적인 소프트웨어 설계를 위해서는 응집도를 높이고 결합도를 낮추는 것이 중요합니다.
  • 높은 응집도는 모듈이 명확하고 관리하기 쉽게 만들어 주며, 낮은 결합도는 시스템의 다른 부분에 영향을 주지 않고 독립적으로 모듈을 수정하거나 교체할 수 있게 해줍니다.
반응형

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

AOP란? AOP 내용, AOP 예제  (0) 2024.01.11
[Querydsl] DTO 조회 방법  (0) 2023.12.17
OAuth2란? OAuth2 예시  (1) 2023.12.15
DTO <-> Entity 변환 이유!  (0) 2023.12.14

Projection 연산이란?

한 Relation의 Attribute들의 부분 집합을 구성하는 연산자입니다.
결과로 생성되는 Relation은 스키마에 명시된 Attribute들만 가집니다.
결과 Relation은 기본 키가 아닌 Attribute에 대해서만 중복된 tuple들이 존재할 수 있습니다.
-> 테이블에서 원하는 컬럼만 뽑아서 조회하는 것

  • Relation 데이터를 원자 값으로 갖는 이차원 테이블
  • Column = Attribute

Querydsl DTO로 조회하는 4가지 방법

  1. Projection.bean
  2. Projection.fields
  3. Projection.constructor
  4. @QueryProjection

사전 작업

  • spring boot3.0

  • build.gradle

    buildscript {
      ext {
          queryDslVersion = "5.0.0"
      }
    }
    

plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'

// querydsl관련 명령어를 gradle탭에 생성해준다. (권장사항)
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"

}

group = 'com.demo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// querydsl 디펜던시 추가
// == 스프링 부트 3.0 이상 ==
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

}

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

/*

  • queryDSL 설정 추가
  • /
    // querydsl에서 사용할 경로 설정
    def querydslDir = "$buildDir/generated/querydsl"
    // JPA 사용 여부와 사용할 경로를 설정
    querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
    }
    // build 시 사용할 sourceSet 추가
    sourceSets {
    main.java.srcDir querydslDir
    }
    // querydsl 컴파일시 사용할 옵션 설정
    compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
    }
    // querydsl 이 compileClassPath 를 상속하도록 설정
    configurations {
    compileOnly {
      extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
    }

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



- Entity

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Users {
@Id
private Long userNo;
private String userId;
private String userName;
}

@Getter @Setter
@NoArgsConstructor
@RequiredArgsConstructor
public class Board {
@Id
private Long id;
private String title;
private String content;
}


- DTO

@Getter @Setter
@NoArgsConstructor
public class UsersDto {
private Long userNo;

private String userId;

private String userName;

@QueryProjection
public UsersDto(Long userNo, String userId, String userName) {
    this.userNo = userNo;
    this.userId = userId;
    this.userName = userName;
}

}


- JpaQueryFactory Config

@Configuration
@RequiredArgsConstructor
public class QuerydslConfiguration {

private final EntityManager em;

@Bean
public JPAQueryFactory jpaQueryFactory(){
    return new JPAQueryFactory(em);
}

}


#### 1. Projections.bean (Setter 메서드를 이용한 조회 방법)

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> users = queryFactory
        .select(Projections.bean(UsersDto.class,
            users.userNo,
            users.userId,
            users.userName
        ))
        .from(users)
        .fetch();
}

}


- Projection.bean 방법은 setter 메서드를 기반으로 동작합니다.
DTO 객체의 각 필드에 setter 메서드가 있어야 합니다.
만약 영속화된 데이터를 변경하는 책임을 가진 객체라면 문제가 될 수 있기 때문에 권장되는 패턴은 아닙니다.

#### 2. Projections.fields (Reflection을 이용한 조회 방법)

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> users = queryFactory
        .select(Projections.fields(UsersDto.class,
            users.userNo,
            users.userId,
            users.userName.as("userName")
        ))
        .from(users)
        .fetch();
}

}


- Porjections.fields를 이용한 방법은 field에 값을 직접 주입해주는 방식입니다.
Projections.bean 방식과 마찬가지로 Type이 다를 경우 매칭되지 않으며, 컴파일 시점에서는 에러를 잡지 못하고 런타임 시점에서 에러가 잡힙니다.

- 만약 Users Entity의 필드가 userName이 아니라면 users.userName.as("userName") 처럼 alias(별칭)을 사용하여 매핑 문제를 해결할 수 있습니다.

#### 3. Projections.constructor (생성자를 이용한 조회 방법)

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> users = queryFactory
        .select(Projections.constructor(UsersDto.class,
            users.userNo,
            users.userId,
            users.userName
        ))
        .from(users)
        .fetch();
}

}


- Projections.constructor는 생산자 기반 바인딩입니다. 생성자 기반 바인딩이기 때문에 객체의 불변성을 가져갈 수 있다는 장점이 있지만 바인딩 과정에서 문제가 생길 수 있습니다.

public static ConstructorExpression constructor(Class type, Expression... exprs){
return new ConstructorExpression(type, exprs);
}


- Projections의 constructor 메서드입니다. 해당 메서드를 보면 DTO 객체의 생성자에게 직접 바인딩하는 것이 아니라 Expression<?>... exprs 값을 넘기는 방식으로 작동됩니다.
따라서 값을 넘길 때 생성자와 필드의 순서를 일치시켜야 합니다.
필드의 개수가 적을 때는 문제가 되지 않지만 필드의 개수가 많아지는 경우 오류로 이어질 수 있습니다.
하지만 생성자를 이용한 방식은 필드명이 달라도 해당 순서에 위치한 필드의 타입만 서로 일치한다면 정상적으로 동작한다는 특징이 있습니다.


#### 4. @QueryProjection

DTO

@Getter @Setter
@NoArgsConstructor
public class UsersDto {
private Long userNo;

private String userId;

private String userName;

@QueryProjection
public UsersDto(Long userNo, String userId, String userName) {
    this.userNo = userNo;
    this.userId = userId;
    this.userName = userName;
}

}

@Repository
@RequiredArgsConstructor
public class UsersRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void findAll(){
    List<UsersDto> dtos = queryFactory
            .select(new QUsersDto(
                    users.userNo,
                    users.userId,
                    users.userName
            ))
            .from(users)
            .fetch();
}

}

```

  • @QueryProjection을 이용하면 불변 객체 선언, 생성자를 그대로 사용할 수 있기 때문에 권장되는 패턴입니다.
    (정확하게는 DTO의 생성자를 사용하는 것이 아니라 DTO 기반으로 생성된 QDTO 객체의 생성자를 사용하는 것입니다.)

  • 작동 방법은 먼저 DTO 생성자에 @QueryProjection 어노테이션을 추가하여 QType의 클래스를 생성하여 위의 예시 코드(UsersDto)와 같이 사용합니다. 이 방식은 new QDTO로 사용하기 때문에 런타임 에러뿐만 아니라 컴파일 시점에서도 에러를 잡아주고, 파라미터로 확인할 수 있다는 장점이 있습니다.
    반면 해당 기능을 사용하려는 DTO마다 QType의 Class를 추가로 생성해줘야 하는 것도 있지만, DTO는 Repository 계층의 조회 용도뿐만 아니라 Service, Controller 모두 사용되기 때문에 @QueryProjection 어노테이션을 DTO에 적용시키는 순간, 모든 계층에서 쓰이는 DTO가 Querydsl에 의존성을 가지기 때문에 아키텍처적으로 적용을 생각해 볼 필요는 있습니다.

반응형

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

AOP란? AOP 내용, AOP 예제  (0) 2024.01.11
응집도와 결합도란?  (1) 2023.12.26
OAuth2란? OAuth2 예시  (1) 2023.12.15
DTO <-> Entity 변환 이유!  (0) 2023.12.14

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