Aop관련한 자세한 설명은 다음 자료를 참고하자

https://harmony-raccoon.tistory.com/22

 

Spring에서 AOP로 Log 기능 구현하기

서버에서 Log 기능은 매우 중요합니다. 예상치 못한 에러로 인해 서버가 다운되거나, 유저 요청이 특정 로직을 통과하지 못했을 때 Log를 통해 데이터를 복구하고 원인을 분석할 수 있습니다.그렇

harmony-raccoon.tistory.com

AOP에서 지원하고 있는 기능 중 하나는 MultiThread 구현이다.

MultiThread를 구현하는 어노테이션은 @Async이다.


@Async

Async는 Spring에서 MultiThread를 도입하기 위해서 필요한 어노테이션이다.

Async를 이용해서 MultiThread를 도입하기 위한 방법은 크게 어노테이션만 달기, Configuration 작성 2가지로 구분된다.

Annotation만 달기

1. Application.java 클래스에 @EnableAsync라는 어노테이션을 달아준다.

@EnableAsync
@SpringBootApplication
public class ExampleApplication {
	public static void main(String[] args) {
		SpringApplication.run(ExampleApplication.class, args);
	}
}

2. 그 후, MultiThread기능을 달고 싶은 메서드에 @Async라는 어노테이션을 달아준다.

@Service
@Slf4j
public class AsyncService {
    @Async
    public void log() throws InterruptedException {
        log.info("{} 쓰레드 작업 시작", Thread.currentThread().getId());
        Thread.sleep(100);
        log.info("{} 쓰레드 작업 종료", Thread.currentThread().getId());
    }
}

이렇게만 하면 끝이다.

그럼 위 log()라는 메서드가 실행될 때, MultiThread가 생성되어 실행된다.

테스트코드는 다음과 같다.

@SpringBootTest
class AsyncServiceTest {
    @Autowired
    AsyncService asyncService;

    @Test
    @DisplayName("멀티쓰레드 확인")
    void testMultiThread() throws InterruptedException {
        for(int i = 0; i < 20; i++) {
            asyncService.log();
        }
    }
}

위의 테스트 코드를 실행하면 결과는 다음과 같다.

테스트 결과

이렇게 쓰레드가 OS 자체의 Scheduling에 따라서 실행되고 있다는 것을 알 수 있다.


Configuration 작성

1. Configuration 클래스 생성 및 @Configuration, @EnableAsync 어노테이션 달아준다. (이때, 위에서 Application.java 파일에 달았던 @EnableAsync는 삭제해도 똑같이 작동한다.)

@EnableAsync
@Configuration
public class AsyncEx {
}

2. 그 후, MultiThread기능을 달고 싶은 메서드에 @Async라는 어노테이션을 달아준다.

 

@Service
@Slf4j
public class AsyncService {
    @Async
    public void log() throws InterruptedException {
        log.info("{} 쓰레드 작업 시작", Thread.currentThread().getId());
        Thread.sleep(100);
        log.info("{} 쓰레드 작업 종료", Thread.currentThread().getId());
    }
}

이렇게 하면 Configuration을 이용한 MultiThread 기능 사용은 끝이다.

내가 작성한 테스트코드는 위에서 알아보았던 코드와 일치한다. 위의 테스트 코드를 다시 작동하면 결과는 다음과 같다.

Configuration을 활용한 테스트 결과


ThreadPool 사용

위에서 작성하였던 Configuration에 ThreadPool을 도입해서 개발자 직접 Thread의 개수, Task Queue의 크기 지정 등 Thread 설정을 할 수 있다.

Configuration 코드를 다시 보면 다음과 같다.

@EnableAsync
@Configuration
public class AsyncEx {
    @Bean(name = "multiThread Ex") // 이름을 설정하면, 해당하는 이름을 가진 @Async 어노테이션에만 이 쓰레드 설정이 적용.
    public Executor multiThreadExecutor() {
        ThreadPoolTaskExecutor poolExecutor = new ThreadPoolTaskExecutor();
        poolExecutor.setCorePoolSize(10); // 최소 쓰레드 수
        poolExecutor.setMaxPoolSize(100); // 최대 쓰레드 수
        poolExecutor.setQueueCapacity(100); // 쓰레드 이상의 작업 요청이 있다면, 이 Queue 에 저장한 후 쓰레드가 종료되면 여기서 작업을 꺼내서 다시 실행.
                                            // 이 Queue 크기를 설정
        poolExecutor.setThreadNamePrefix("MultiThread -"); // 쓰레드 실행시 이름을 설정
        poolExecutor.initialize();

        return poolExecutor;
    }
}

이렇게 설정한 후, 다시 테스트 코드를 돌리면 결과는 다음과 같다.

ThreadPool를 활용한 테스트 결과

위의 이미지와 다른 점은 Spring 자체에서 Thread로그를 찍을 때, Task가 아닌 우리가 설정한 MultiThread로 시작한다는 점이다.

즉, 위의 설정이 제대로 작동하고 있다.

참고 자료

https://cano721.tistory.com/208

 

[Spring] @Async 비동기 멀티스레드 사용법

수정사항 2022-08-27 async 사용 시 비동기 스레드 exception 처리 CompletableFuture 사용법 추가 Async 사용계기 현재 마이다스 AI 역검 백엔드팀에 들어오게 되었는데, 과제 중 원활한 검증작업을 위한 응시

cano721.tistory.com

https://320hwany.tistory.com/107

 

Spring에서 멀티 쓰레드 비동기 프로그래밍 해보기

Spring은 기본적으로 멀티 쓰레드, 동기 방식으로 작동합니다. 하지만 성능 향상을 위해서 비동기 방식으로 작동을 하도록 할 수 있습니다. 이번 글에서는 싱글 쓰레드/멀티 쓰레드, 동기/비동기

320hwany.tistory.com

https://hseong.tistory.com/88

 

스프링 @Async를 이용한 비동기 실행 적용하기

Spring Async@Async비동기 실행을 위한 @Async를 사용하기 위해서는 먼저 적당한 @Configuration 클래스에 @EnableAsync를 추가해주어야 합니다.@EnableAsync @Configuration public class AsyncConfig {}그런 다음 비동기적으

hseong.tistory.com

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Spring > AOP' 카테고리의 다른 글

Spring에서 AOP로 Log 기능 구현하기  (0) 2024.12.13

MVC패턴에서 테스트 코드는 크게 3가지로 분류됩니다. Controller 테스트 코드, Service 테스트 코드 그리고 Repository 테스트 코드입니다.

여기서 Controller 테스트와 Service 테스트는 실제 데이터를 받아서 처리하지 않고 Mock으로 처리하여 진행하게 됩니다.

실제로 계층을 나눈 이유는 이러한 독립성을 유지하기 위해서 입니다.

이 글에서는 MongoDB와 Repository코드에서 어떻게 테스트 코드를 작성하는 방법을 소개합니다.


코드

@DataMongoTest
class DomainRepositoryTest {
    private DomainRepository repository;

    @Autowired
    public DomainRepositoryTest(DomainRepository repository) {
        this.repository = repository;
    }

    @Test
    void Domain_정상_저장() {
        Domain domain = new Domain("테스트 데이터");

        repository.save(domain);
        Domain domain = repository.findByEmail("테스트 데이터");

        assertNotNull(domain);
        assertEquals("테스트 데이터", domain.getEmail());
        repository.deleteDomain(domain);
    }

코드 해석

@DataMongoTest

Mongo Test 어노테이션으로 전 글에서 작성했던 Embedded Mongo Test를 들고 와서 설정합니다.

생성자

@Autowired
public DomainRepositoryTest(DomainRepository repository) {
    this.repository = repository;
}
여기서 생성자는 Bean으로 등록된 Repository를 테스트 코드에 주입해줍니다. 이때, Repository는 Embedded DB와 연결되어 있습니다.
더보기

이때, DomainRepository는 MongoRepository<Domain, IDType>을 확장받은 Interface이여야 한다.

public interface DomainRepository extends MongoRepository<Domain, BigInteger> {
    Domain findByEmail(String name);
}

테스트 코드

@Test void Domain_정상_저장() {
    Domain domain = new Domain("테스트 데이터");
    repository.save(domain);

    Domain domain = repository.findByEmail("테스트 데이터");

    assertNotNull(domain);
    assertEquals("테스트 데이터", domain.getEmail());
    repository.deleteDomain(domain);
}
Domain domain = new Domain("테스트 데이터");
repository.save(domain);
처음에 2줄은 데이터를 생성합니다.

Domain domain = repository.findByEmail("테스트 데이터");
저장된 데이터를 다시 들고 옵니다. (이때, 저장한 공간은 Embedded DB이므로, 테스트 객체가 살아있는 동안에만 메모리로 저장하고, 테스트 객체가 자원을 반납하면 데이터도 삭제됩니다.
따라서, 여러개의 테스트를 같은 객체 내에서 동시에 작동시킨다면 데이터의 충돌을 생각하여 테스트 데이터를 작성해야 합니다.

assertNotNull(domain);
assertEquals("테스트 데이터", domain.getEmail());
repository.deleteDomain(domain);
Embedded 데이터에서 나온 객체와 DB에 저장하기 전 데이터가 같은지 확인합니다. 이때, assertThat같은 함수는 junit의 테스트 프레임워크를 사용하였습니다.

마무리

이렇게 Repository의 테스트 코드를 작성하는 방법에 대해서 살펴보았습니다.

항상 테스트 데이터는 실제 DB에 반영되서는 안되며, 테스트를 위한 DB를 따로 설정하거나 이번에 포스팅한 내용처럼 MemoryDB를 사용해야 합니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

Mockito란?

Mockito는 간단한 설정만으로 가짜(Mock) 객체를 생성하여 로직을 테스트할 수 있게 해주는 테스트 프레임워크입니다.
대부분의 객체 지향 프로그래밍(OOP) 기반 서비스는 다양한 의존 관계를 통해 기능을 제공하는데, 이런 의존 관계가 테스트를 어렵게 만드는 경우가 많습니다.
Mockito는 이러한 의존 관계를 가짜 객체(Mock)로 대체하여, 테스트하려는 핵심 로직에만 집중할 수 있도록 도와줍니다.


Mockito 주요 어노테이션

1. @Mock

  • 가짜 객체를 생성합니다.
  • 테스트하려는 클래스의 의존 관계를 대체하기 위해 사용됩니다.

2. @InjectMocks

  • @Mock 또는 @Spy로 생성된 객체를 테스트하려는 클래스에 자동으로 주입합니다.
  • 의존 관계가 자동으로 설정되므로 수동으로 객체를 생성하거나 주입할 필요가 없습니다.

3. @Spy

  • 원본 객체를 생성한 뒤, Stubbing되지 않은 메서드는 실제 메서드를 실행합니다.
  • 기본 생성자(NoArgsConstructor)가 필요합니다.

Mockito를 적용한 테스트 코드

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @InjectMocks
    private UserService userService;

    @Mock
    private UserRepositoryValid repository;

    private final String userEmail = "admin@gmail.com";

    @Test
    void 새로운_유저_생성() {
        GetNewUserDto getNewUserDto = 유저_데이터_생성();
        userService.createNewUser(getNewUserDto);

        when(userService.findByUserEmail(userEmail)).thenReturn(stubbing());

        User user = userService.findByUserEmail(userEmail);
        assertThat(user.getEmail()).isEqualTo(userEmail);
    }
    
    GetNewUserDto 유저_데이터_생성() {
        return new GetNewUserDto(userEmail, "admin", "1234", "Student", "Default");
    }

    User stubbing() {
        return new User(new CreateNewUserDto(userEmail, "admin", "1234", UserType.Student, SsoType.Default));
    }
}

Stubbing이란?

Stubbing은 테스트 메서드가 실행된 후 어떤 값을 반환해야 하는지 개발자가 명시하는 과정입니다.
즉, "이 메서드가 실행되면, 이러한 결과를 반환한다" 라고 미리 지정하는 것입니다.
Stubbing은 데이터 검증보다는 테스트하려는 로직을 명확히 검증하는 데 중점을 둡니다.

예시:

when(repository.findByEmail(userEmail)).thenReturn(mockUser);
  • repository.findByEmail() 메서드를 호출하면, 실제 DB를 조회하지 않고 미리 설정한 mockUser 객체를 반환합니다.

Mockito 사용 시 주의점

  1. Null 또는 0 값 주의
    • @Mock으로 생성된 객체는 기본적으로 모든 데이터가 null 또는 0으로 초기화됩니다.
    • 따라서 데이터 검증 테스트에는 적합하지 않습니다.
  2. Stubbing 활용
    • 실제 데이터를 확인해야 하는 경우, Stubbing을 통해 예상 결과를 미리 설정할 수 있습니다.

Mockito의 장점

  1. 가짜 클래스를 직접 작성할 필요 없음
    • 간단한 어노테이션으로 Mock 객체를 생성할 수 있습니다.
  2. 독립적인 테스트 구현
    • 의존 관계를 Mock으로 대체해 테스트가 다른 클래스나 서비스에 의존하지 않도록 합니다.
  3. 엣지 케이스 검증 용이
    • 복잡한 로직의 엣지 케이스를 효과적으로 검증할 수 있습니다.

마무리

Mockito를 사용하면 테스트를 단순화하고, 로직 검증에 집중할 수 있습니다.
다만, 데이터 검증은 Repository 테스트에서 확인하고, Mockito 테스트에서는 로직 검증에 중점을 두는 것이 효과적입니다.
테스트를 체계적으로 작성하여 더 견고한 코드를 만들어 보세요!

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!


참고 문헌

참고문헌1

참고문헌2

'Spring' 카테고리의 다른 글

Spring에 예외처리 적용하기 With @RestControllerAdvice  (0) 2024.12.12
Spring에 Swagger 적용하기  (0) 2024.12.12
Spring에 DTO 적용하기  (0) 2024.12.09

NoSQL 데이터베이스 중 가장 많이 활용되는 MongoDB는 최근 AI와 빅데이터의 성장과 함께 더욱 주목받고 있습니다. 저의 프로젝트 역시 AI를 활용한 서비스를 제공하기 때문에 MongoDB를 도입하게 되었습니다. 이번 글에서는 Spring 환경에서 MongoDB를 설정하는 방법을 소개합니다.

MongoDB 설치하기

Gradle를 통해서 간단하게 라이브러리를 설치할 수 있습니다.

implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

MongoDB 설정하기

1. 간단한 설정

기본적으로 application.yml 파일에 MongoDB 연결 정보를 다음과 같이 작성하면 설정이 완료됩니다:

spring:
    data:
    	mongodb:
            uri=mongodb://localhost:27017/mydb

이 방식은 간단한 연결이 필요한 경우 유용합니다. 하지만 프로젝트에서 더 복잡한 설정이 필요하다면 MongoDBConfig 클래스를 생성하여 직접 설정할 수 있습니다.

2. 커스텀 설정

추가적인 설정이 필요한 경우 다음과 같이 설정 클래스를 작성합니다:

 

@Configuration
@EnableMongoRepositories(basePackages = "repository가 저장된 위치")
public class MongoDBConfig extends AbstractMongoClientConfiguration {
    @Value("${spring.data.mongodb.url}")
    private String connectionUrl;

    @NotNull
    public MongoClient mongoClient() {
        ConnectionString connectionString = new ConnectionString(connectionUrl);
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
                .uuidRepresentation(UuidRepresentation.STANDARD)
                .applyConnectionString(connectionString)
                .build();
        return MongoClients.create(mongoClientSettings);
    }

    @NotNull
    @Override
    protected String getDatabaseName() {
        return "mydb";
    }
}

이 방식은 프로젝트의 요구사항에 맞게 세부적인 설정을 적용할 수 있습니다. 특히, uuidRepresentation과 같은 속성은 데이터베이스 보안을 강화하는 데 유용합니다.

 

마무리

이번 글에서는 Spring에서 MongoDB를 설정하는 두 가지 방법을 살펴보았습니다. 간단한 프로젝트는 application.yml 파일을 통해 빠르게 설정할 수 있고, 복잡한 설정이 필요한 경우 MongoDBConfig 클래스를 활용하면 유연하게 대응할 수 있습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

서버에서 Log 기능은 매우 중요합니다. 예상치 못한 에러로 인해 서버가 다운되거나, 유저 요청이 특정 로직을 통과하지 못했을 때 Log를 통해 데이터를 복구하고 원인을 분석할 수 있습니다.

그렇다면 모든 로직마다 Log 기능을 추가해야 할까요?

매번 동일한 코드를 구현한다면 개발자가 실수할 가능성이 높아지고, 코드를 이해하기도 어려워질 것입니다. 이를 해결하기 위한 패러다임이 바로 AOP(Aspect-Oriented Programming)입니다.


AOP(Aspect-Oriented Programming)란?

AOP관심사 분리(Separation of Concerns)를 통해 코드의 유지보수성을 높이는 프로그래밍 패러다임입니다.

관심사 분리란 무엇인가?

코드를 설계할 때는 보통 두 가지로 구분하여 고민합니다:

  1. 핵심 로직: 요구사항을 처리하는 주요 로직.
  2. 부가 로직: 서비스의 정확한 동작을 지원하기 위한 부가적인 로직.

핵심 로직은 요구사항마다 달라지므로 개발자가 집중해서 개발해야 합니다. 반면, 부가 로직은 여러 요구사항에서 공통적으로 사용되는 기능입니다. 부가 로직을 매번 구현하기보다는 한 번 구현하여 핵심 로직에 적용할 수 있다면, 개발의 효율성과 유지보수성이 높아질 것입니다.

이러한 아이디어에서 출발한 패러다임이 바로 AOP입니다.


Spring AOP 설치하기

Gradle를 통해서 간단하게 라이브러리를 설치할 수 있습니다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

PointCut: 부가 로직 적용 대상 지정

AOP에서는 PointCut을 사용하여 부가 로직이 적용될 대상을 명시합니다. 쉽게 말해, 부가 로직이 실행될 객체와 메서드를 지정하는 것입니다.

예제 코드

@Slf4j
@Aspect
@Component
public class LogPointcut {
    @Pointcut("execution(* eumsun.backend.controller.NotTokenController.*(..))")
    public void notToken(){
    }
}

 

코드 분석

  • @Aspect: 해당 클래스가 AOP 관련 코드를 포함하고 있음을 명시합니다. 이 어노테이션이 붙은 클래스는 Advisor로 등록됩니다.
  • @Pointcut: 부가 로직이 적용될 대상을 지정하며, AspectJ 표현식을 사용합니다.

Advice: 부가 로직 실행 시점 지정

Advice는 부가 로직이 실행되는 시점을 지정하는 어노테이션입니다.

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {
    private final LogService logService;

    @Around("eumsun.backend.config.log.LogPointcut.notToken()")
    public Object beforeAdviceNotTokenLog(ProceedingJoinPoint joinPoint) throws Throwable {
        return tryLog(joinPoint, this::notTokenLogging);
    }
    
    private Object tryLog(ProceedingJoinPoint joinPoint, Consumer<ProceedingJoinPoint> logMethod) throws Throwable {
        Object result = joinPoint.proceed();
        logMethod.accept(joinPoint);
        return result;
    }
    
    private void notTokenLogging(ProceedingJoinPoint joinPoint) {
        String requestBody = getRequestBody();
        viewLog(joinPoint, requestBody);
        createLog(joinPoint, requestBody);
    }
}

 

코드 분석

  1. @Around:
    • 부가 로직이 핵심 로직의 전후에 실행됩니다.
    • 가장 강력한 Advice 어노테이션입니다.
  2. ProceedingJoinPoint:
    • 유저 요청에 대한 정보를 담고 있는 객체로, 핵심 로직을 실행하거나 파라미터 정보를 얻을 수 있습니다.

만약 RequestBody를 Log에 찍고 싶다면 다음과 같은 클래스를 추가해야 한다.

더보기
더보기

RequestBody 캐싱을 위한 설정

유저의 RequestBody를 로그에 기록하려면 추가 설정이 필요합니다. 기본적으로 RequestBody는 한 번 읽으면 사라지기 때문에 캐싱해야 합니다.

필터 추가

@Component
public class RequestCacheFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        filterChain.doFilter(request, response);
    }
}

코드 분석

  • OncePerRequestFilter를 상속하여 RequestBody를 캐싱합니다.
  • ContentCachingRequestWrapper를 사용해 데이터를 한 번 읽어도 이후에 다시 사용할 수 있도록 만듭니다.

마무리

AOP를 활용하여 부가 로직핵심 로직을 분리하면, 개발자는 부가 로직에 시간을 할애하지 않고 핵심 로직에 집중할 수 있습니다. 이를 통해 더욱 완성도 높은 서비스를 제공할 수 있습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Spring > AOP' 카테고리의 다른 글

[Spring] AOP로 MultiThread 구현하기  (1) 2025.05.28

서비스를 개발하다 보면 로직이 예상과 다르게 실행되거나, 예상하지 못한 에러가 발생하는 경우가 있습니다. 이러한 예외를 적절히 처리하지 않으면 복구가 어려울 뿐 아니라, 서비스를 이용하는 유저들의 경험에도 부정적인 영향을 미칠 수 있습니다.

따라서 예외가 어디서 발생했는지, 어떤 요청을 통해 처리해야 하는지를 유저나 프론트엔드 개발자에게 명확히 전달할 필요가 있습니다. 이를 지원하는 기능이 바로 @RestControllerAdvice입니다.


@ControllerAdvice란?

Spring에서는 특정 메서드에서 발생한 예외를 처리하지 않으면, 해당 예외는 부모 메서드로 던져지며 호출 스택을 타고 최종적으로 Servlet까지 전달됩니다. Servlet까지 예외가 올라가면 유저는 Whitelabel Error Page를 보게 됩니다. 이는 서비스의 완성도를 낮추고, 유저 경험을 저하시킬 수 있습니다.

API 서버의 경우 이러한 상황을 방지하기 위해 적절한 예외 응답을 반환해야 합니다. 예외 응답은 발생한 오류를 프론트엔드에 명확히 전달함으로써 문제 해결을 돕고, 유저 경험을 보호합니다.


커스텀 예외 처리 (Custom Exception)

예외를 명확히 이해할 수 있도록 이름으로 표현하는 방식을 사용할 수 있습니다. 아래는 실제 프로젝트에서 사용된 커스텀 예외 클래스입니다.

public class NotHaveToken extends RuntimeException {
    public NotHaveToken(String message) {
        super(message);
    }
}

위와 같이 커스텀 예외 클래스를 작성하면, 해당 예외의 이름만으로 어떤 문제가 발생했는지 쉽게 파악할 수 있습니다. 이후 로직에서 이 예외를 적절히 발생시킵니다.

Spring은 발생한 예외를 처리하는 메서드가 없으면, 호출 스택을 타고 올라가면서 처리 가능한 메서드를 탐색합니다. 만약 @ControllerAdvice 또는 @RestControllerAdvice를 사용한다면, 해당 예외를 캐치하여 처리할 수 있습니다.


@RestControllerAdvice 적용 코드

아래는 실제 프로젝트에서 사용된 @RestControllerAdvice를 활용한 예외 처리 코드입니다.

@Slf4j
@RestControllerAdvice
public class NotTokenControllerException {
    @ExceptionHandler(NotHaveToken.class)
    public API<String> NotHaveTokenExHandler(NotHaveToken e) {
        log.error("[NotHaveToken] ex = {}", e.getMessage());
        return new API<>(APIErrorMessage.토큰_요청);
    }
}

코드 분석

  1. @RestControllerAdvice
    • @ControllerAdvice에 @ResponseBody가 추가된 어노테이션입니다.
    • 특정 예외가 발생하면 이 어노테이션이 붙은 클래스에서 해당 예외를 처리할 수 있는 메서드를 탐색합니다.
    • 만약 처리할 메서드가 없다면 기존 Spring 예외 처리 흐름을 따르게 됩니다.
  2. @ExceptionHandler(NotHaveToken.class)
    • 처리할 예외 타입을 지정합니다.
    • 위 코드에서는 NotHaveToken 예외를 처리하기 위한 메서드로 정의되어 있습니다.

마무리

@RestControllerAdvice를 활용하면 발생한 예외의 원인과 처리 방안을 명확히 정의하고, 이를 클라이언트에 전달할 수 있습니다. 이를 통해 프론트엔드와 백엔드의 소통을 개선하고, 유저 경험을 저하시키지 않으면서도 완성도 높은 서비스를 제공할 수 있습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Spring' 카테고리의 다른 글

Spring에서 Mockito를 활용해 Test Code 작성하기  (2) 2025.01.03
Spring에 Swagger 적용하기  (0) 2024.12.12
Spring에 DTO 적용하기  (0) 2024.12.09

백엔드와 프론트엔드가 함께 협업하면서 가장 중요한 소통 도구 중 하나는 바로 API 문서입니다. API 문서는 백엔드가 제공하는 데이터의 형태와 이를 얻기 위해 필요한 파라미터를 명확히 기술한 문서로, 효과적인 소통을 위해 필수적입니다.

하지만, 개발과 동시에 API 문서를 작성하는 것은 쉽지 않습니다. 이를 해결하기 위해 등장한 것이 바로 Swagger 라이브러리입니다. Swagger는 API 문서를 자동으로 생성해 줄 뿐만 아니라, 문서 내에서 API 테스트를 진행할 수 있는 기능도 제공합니다. 이번 글에서는 Swagger의 개념과 실제 프로젝트에서 사용한 설정 코드를 소개합니다.


Swagger란?

Swagger는 API를 정확하게 명시하는 문서를 자동으로 생성해 주는 라이브러리로, 다음과 같은 기능을 제공합니다:

  • API 명세 문서 생성: REST API의 구조와 사용 방법을 명확히 정의합니다.
  • API 테스트 기능: 생성된 문서에서 API 호출 테스트를 실행할 수 있습니다.

이를 통해 개발자는 더욱 효율적으로 작업할 수 있으며, 프론트엔드와의 협업 과정에서 발생할 수 있는 혼란을 줄일 수 있습니다.


Swagger 설치

Gradle로는 다음과 같은 코드로 간단하게 라이브러리를 다운받을 수 있습니다.

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

Swagger Config

다음은 Swagger를 사용하여 API 테스트를 위한 설정을 정의한 코드입니다. 실제 프로젝트에서 사용한 예제를 기반으로 설명하겠습니다.

@OpenAPIDefinition(
        info = @Info(
                title = "이음선 프로젝트 API 명세서",
                description = "이음선 프로젝트에 사용되는 API 명세서",
                version = "v1"
        )
)

@Configuration
public class SwaggerConfig {
    private static final String BEARER_TOKEN_PREFIX = "Bearer";

    @Bean
    public OpenAPI openAPI() {
        String jwtSchemeName = HEADER;
        SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtSchemeName);
        Components components = new Components()
                .addSecuritySchemes(jwtSchemeName, new SecurityScheme()
                        .name(jwtSchemeName)
                        .type(SecurityScheme.Type.HTTP)
                        .scheme(BEARER_TOKEN_PREFIX)
                        .bearerFormat("JWT"));

        return new OpenAPI()
                .addSecurityItem(securityRequirement)
                .components(components);
    }
}

코드 분석

@OpenAPIDefinition

  • title: API 문서의 제목을 설정합니다.
  • description: API 문서에 대한 설명을 작성합니다.
  • version: API 문서의 버전을 지정합니다.

Bearer Token Prefix

  • JWT 인증에 사용되는 Bearer 토큰의 접두사를 설정합니다.
  • 기본값은 "Bearer"입니다.

SecurityRequirement

  • 각 API 요청이 특정 인증 스키마를 만족해야 한다고 정의합니다.
  • 여기서는 Authorization 헤더를 요구합니다.

addSecuritySchemes

  • 인증 스키마를 정의합니다.
  • name: 인증 헤더 이름.
  • type: 인증 방식 (HTTP로 설정).
  • scheme: Bearer Token 방식을 지정.
  • bearerFormat: 토큰의 형식을 "JWT"로 설정.

addSecurityItem

  • 모든 API 요청이 정의된 인증 스키마를 따르도록 설정합니다.

Components

  • 앞서 정의한 Components 객체를 OpenAPI 객체에 추가합니다.

Swagger UI를 통한 API 문서 확인

Swagger 설정이 완료되면, Swagger UI를 통해 API 문서를 확인할 수 있습니다. 보통 다음과 같은 URL에서 확인할 수 있습니다:

http://localhost:8080/swagger-ui/index.html

Swagger UI는 API 명세서를 사용자 친화적인 형태로 보여주며, 아래와 같은 기능을 제공합니다:

  • API 엔드포인트 목록 및 세부 정보 표시.
  • 각 엔드포인트에 대해 직접 요청을 보내보고 응답 확인.

 

실제 동작하는 API 문서
실제 API 문서 형태


마무리

API 문서를 정확히 명시하면 백엔드와 프론트엔드 개발자 모두 효율적으로 작업할 수 있습니다. Swagger를 활용하면 이러한 문서를 자동으로 생성하고, 테스트까지 지원받을 수 있어 협업과 개발 속도를 크게 향상시킬 수 있습니다.

더 자세한 정보는 SpringDoc 공식 문서를 참고하세요.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

Spring Security와 JWT 인증 시스템 구현하기

이번 글에서는 Spring Security를 활용하여 JWT 인증 시스템을 구현하는 코드를 자세히 살펴보겠습니다.


JwtAuthenticationFilter

다음은 실제 프로젝트에서 사용한 Spring Security의 필터 설정 코드입니다. 이 코드를 통해 주요 구성 요소를 하나씩 분석해 보겠습니다.

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
    private final JwtProvider jwtProvider;
    private final SecurityProperties securityProperties;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        if(verifySkipFilterChain(requestURI)){
            chain.doFilter(request, response);
            return;
        }

        String token = jwtProvider.getToken(httpRequest);
        validToken(token);
        chain.doFilter(request, response);
    }
    
    private Boolean verifySkipFilterChain(String requestUrl) {
        return securityProperties.getSkipPatterns().stream()
                .anyMatch(pattern -> pathMatcher.match(pattern, requestUrl));
    }

    private void validToken(String token) {
        if(!verifyToken(token)){
            return;
        }
        Date iat = jwtProvider.getIat(token);
        Authentication authentication = jwtProvider.getAuthentication(token, iat);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

코드 분석

  1. GenericFilterBean 커스텀
    • GenericFilterBean을 상속받아 Spring Security의 필터 체인에서 커스텀 인증 로직을 구현합니다.
  2. JwtProvider
    • JWT와 관련된 모든 작업(토큰 생성, 검증, 정보 추출 등)을 처리하는 객체입니다.
  3. SecurityProperties
    • 인증/인가 절차를 생략해야 할 URL 패턴을 관리하는 설정 객체입니다.
  4. doFilter
    • 필터 체인의 핵심 메서드로, 요청이 필터를 통과할 때 실행됩니다. 이 메서드를 재정의하여 원하는 인증/인가 로직을 구현할 수 있습니다.
  5. verifySkipFilterChain
    • 인증/인가 절차를 생략해야 할 URL인지 확인하는 메서드입니다.
    • 설정된 URL 패턴을 기반으로 필터 체인을 건너뛸지 결정합니다.
  6. validToken
    • 요청의 헤더에서 토큰을 추출하고, 토큰의 유효성을 검증합니다.
    • 검증이 완료되면 해당 사용자 정보를 SecurityContextHolder에 저장합니다

PrincipalDetailsService

PrincipalDetailsService는 Spring Security에서 사용하는 인증 객체를 제공하는 서비스 클래스입니다. 아래는 실제 프로젝트에서 사용된 코드입니다.

@Service
@RequiredArgsConstructor
public class PrincipalDetailsService {
    private final UserDataRepositoryValid repository;

    public UserDetails loadUserByUserId(String userId, Date iat) throws UsernameNotFoundException {
        UserData userData = repository.findUserById(userId);
        Date updateAt = tranformdate(userData);

        if(iat.before(updateAt)){
            throw new ExpiredToken(APIErrorMessage.유저_변경_후_토큰_만료.getMessage());
        }
        return new PrincipalDetails(userData);
    }

    private Date tranformdate(UserData userData) {
        LocalDateTime update = userData.getUpdateAtPassword();
        ZonedDateTime updateTime = update.atZone(ZoneId.systemDefault());
        return Date.from(updateTime.toInstant());
    }
}

 

코드 분석

  1. loadUserByUserId
    • 토큰에서 추출한 사용자 ID로 데이터베이스에서 유저 정보를 조회합니다.
    • 토큰 발행 시각(iat)이 비밀번호 변경 시각 이후인지 확인하여 유효성을 검증합니다.
  2. transformDate
    • LocalDateTimeDate 객체로 변환하는 유틸리티 메서드입니다.
  3. 예외 처리
    • 토큰이 만료되었거나 유효하지 않은 경우 예외를 던져 비즈니스 로직에 진입하지 못하도록 차단합니다.

PrincipalDetails

PrincipalDetails는 Spring Security에서 제공하는 UserDetails를 구현한 클래스입니다. 아래는 구현 코드입니다.

public class PrincipalDetails implements UserDetails {
    private final UserData userData;

    public PrincipalDetails(UserData userData) {
        this.userData = userData;
    }

    public UserData getUserData() {
        return userData;
    }

    public String getUserEmail() {
        return userData.getEmail();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        String userType = userData.getUserType().toString();
        return Arrays.stream(userType.split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    } // userType으로 권한을 조회하기 위한 코드

    @Override
    public String getPassword() {
        return userData.getPassword();
    }

    @Override
    public String getUsername() {
        return userData.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true; // 이 계정 만료되었는지 확인할꺼니?
    }

    @Override
    public boolean isAccountNonLocked() {
        return true; // 계정 잠김 되어있니?
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 비밀번호 변경 기간 설정할꺼니?
    }

    @Override
    public boolean isEnabled() {
        return true; // 계정 활성화 되어있니?
        // 1년동안 로그인 안하면 휴면계정으로 들어가냐?
    }
}

코드 분석

  1. getAuthorities
    • 사용자 권한을 조회하여 GrantedAuthority 리스트를 반환합니다.
  2. isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled
    • 계정의 상태를 확인하는 메서드들로, 서비스 요구사항에 따라 구현할 수 있습니다.

마무리

이번 글에서는 Spring Security와 JWT 인증 시스템을 구현하는 코드를 분석했습니다. 다양한 객체가 상호작용하며 인증과 인가 로직을 처리하는 모습을 보며, 객체지향 프로그래밍(OOP)의 원칙을 준수한 설계의 중요성을 다시금 느낄 수 있었습니다.

Spring Security는 복잡하지만 강력한 인증/인가 시스템을 제공합니다. 이러한 구조를 이해하고 활용하면 더 안전하고 효율적인 애플리케이션을 개발할 수 있습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Spring > Security' 카테고리의 다른 글

Spring에 Security 적용하기 1  (1) 2024.12.11

웹 애플리케이션에서 인증 및 인가를 구현하려고 하면 빠지지 않고 등장하는 것이 바로 Spring Security입니다. 이 글에서는 Spring Security가 무엇인지, 어떤 역할을 하는지, 그리고 이를 프로젝트에 어떻게 적용할 수 있는지 알아보겠습니다.

Spring Security

Spring Security는 인증과 인가를 처리하기 위해 제공되는 강력한 라이브러리로, 필터 체인(filter chain)을 기반으로 작동합니다. 이를 통해 사용자 인증 및 권한 부여를 관리할 뿐만 아니라 다양한 보안 공격으로부터 애플리케이션을 보호합니다.

Spring Security의 주요 특징은 다음과 같습니다:

  • 인증 및 권한 부여에 대한 포괄적이고 확장 가능한 지원
  • 세션 고정, 클릭재킹, 크로스 사이트 요청 위조(CSRF) 등의 보안 공격 방지
  • 서블릿 API 통합
  • Spring Web MVC와의 선택적 통합

인증과 인가의 차이

Spring Security를 이해하려면 먼저 인증인가의 차이를 명확히 알아야 합니다.

인증 (Authentication)

인증은 사용자가 "내 서비스에 등록된 회원인지"를 확인하는 절차입니다. 사용자가 회원임을 증명하는 과정으로, 흔히 아이디와 비밀번호, 혹은 OAuth 등을 통해 처리됩니다.

인가 (Authorization)

인가는 "이 사용자가 특정 작업이나 리소스에 접근할 권한이 있는지"를 결정하는 과정입니다. 예를 들어, 회사에서 CEO가 접근할 수 있는 정보와 신입 직원이 접근할 수 있는 정보는 다를 수 있습니다. 이러한 권한 관리를 인가라고 합니다.


필터(Filter)와 필터 체인(Filter Chain)

filter chain

Spring Security는 필터 체인(filter chain)을 활용해 인증 및 인가 과정을 처리합니다. 여기서 필터는 클라이언트 요청이 서버에 도달하기 전에 미리 처리해야 할 기능을 수행합니다

 

Spring은 서블릿에서 제공하는 기본 필터를 확장해, 인증과 인가 시스템을 손쉽게 구현할 수 있도록 지원합니다. 이를 기반으로 Spring Security는 강력한 보안 기능을 제공합니다.

 

필터 체인을 간단히 그림으로 표현하면 다음과 같은 구조를 가집니다:

Spring Security


Security 설치하기

Gradle를 통해서 간단하게 라이브러리를 설치할 수 있습니다.

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

Security Config 설정하기

다음은 실제 프로젝트에서 사용한 Spring Security 설정 코드입니다. 코드를 보며 주요 구성 요소를 살펴보겠습니다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {
    private final JwtProvider jwtProvider;
    private final SecurityProperties securityProperties;

    private final String develop = UserType.DEVELOP.name();

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .httpBasic(HttpBasicConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .formLogin(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/develop/**").hasAuthority(develop)
                        .anyRequest().permitAll()
                )
                .addFilterBefore(new JwtAuthenticationFilter(jwtProvider, securityProperties),
                        UsernamePasswordAuthenticationFilter.class)
                .build();
    }
}

Config Annotation

@Configuration

  • 이 클래스가 Spring 설정 클래스임을 나타냅니다. 또한, 메서드를 통해 Bean을 등록할 수 있습니다.

@EnableWebSecurity

  • Spring Security를 활성화하고, 프로젝트에서 Security 설정을 적용하도록 합니다.

@RequiredArgsConstructor

  • final 키워드가 붙은 필드를 초기화하는 생성자를 자동으로 생성합니다. 이를 통해 의존성을 간단히 주입할 수 있습니다.

@EnableMethodSecurity

  • 메서드 단위로 Security 필터를 설정할 수 있도록 지원합니다. 예를 들어, 특정 메서드에만 인증/인가를 적용할 수 있습니다.

SecurityFilterChain 설정하기

주요 메서드 설명

  1. httpBasic(HttpBasicConfigurer::disable)
    • Http Basic 인증을 비활성화합니다. 토큰 기반 인증을 사용할 경우 필요하지 않습니다.
  2. csrf(AbstractHttpConfigurer::disable)
    • CSRF(Cross-Site Request Forgery) 방어를 비활성화합니다. API 서버에서는 일반적으로 필요하지 않기 때문에 비활성화합니다.
  3. sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    • 세션을 생성하지 않고 상태를 유지하지 않도록 설정합니다. JWT를 사용하여 인증/인가를 처리하기 때문에 세션이 필요 없습니다.
  4. formLogin(AbstractHttpConfigurer::disable)
    • 폼 기반 로그인을 비활성화합니다. API 서버에서는 폼 로그인을 사용하지 않으므로 필요하지 않습니다.
  5. authorizeHttpRequests
    • 요청 경로에 따른 권한 설정을 정의합니다.
      • /develop/** 경로: develop 권한이 있는 사용자만 접근 가능
      • 그 외 모든 요청: 인증 없이 접근 허용
  6. addFilterBefore(new JwtAuthenticationFilter(jwtProvider, securityProperties), UsernamePasswordAuthenticationFilter.class)
    • Spring Security의 필터 체인에서 UsernamePasswordAuthenticationFilter 앞에 JwtAuthenticationFilter를 추가합니다.
      • JwtAuthenticationFilter: JWT를 검증하고 인증을 처리하는 필터
      • UsernamePasswordAuthenticationFilter: Spring Security의 기본 인증 필터

마무리

Spring Security는 인증과 인가를 처리하기 위한 강력한 도구입니다. 이 글에서는 기본적인 개념과 설정 파일의 주요 구성 요소를 살펴보았습니다. 다음 글에서는 JWT 인증 필터를 직접 구현하며 Spring Security를 활용하는 방법을 자세히 알아보겠습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!


참고 문헌

https://hello-judy-world.tistory.com/216

 

[Spring] Spring Security 개념과 처리 과정 👮‍♀️ (+근데 상황극을 곁들인)

오늘도 노드 마을에서 온.. 토끼는 낯선 기술에 울고 있다..(?) 그렇다.. 유저가 있는 서비스라면 인증과 인가 처리는 필수이다. Spring에서는 Spring Security라는 프레임워크로 관련 기능을 제공하고

hello-judy-world.tistory.com

https://spring.io/projects/spring-security

 

Spring Security

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications. Spring Security is a framework that focuses on providing both authentication and authoriz

spring.io

 

'Spring > Security' 카테고리의 다른 글

Spring에 Security 적용하기 2  (0) 2024.12.12

MVC 패턴을 공부하다 보면, DTO(Data Transfer Object)를 이용해 Domain(Model) 로직을 View로부터 숨겨야 한다는 점을 강조한다. 그렇다면 왜 DTO를 통해 Domain(Model)을 숨겨야 할까?

Domain vs View: DTO가 필요한 이유

먼저 MVC 패턴의 시작점을 이해할 필요가 있다. 많은 서버에서 MVC 패턴을 기반으로 코드를 구현하는 이유는 무엇일까?

바로 수정의 전파를 막고, 유지보수를 수월하게 하기 위해서다.

수정의 전파란 무엇인가?

수정의 전파란, 한 클래스에서 수정이 발생했을 때, 다른 클래스에도 수정이 필요한 상황을 말한다. 이는 객체 간 결합도가 높은 경우에 자주 발생한다.

MVC 패턴에서는 이 결합도를 낮추는 것이 중요하며, 그 방법 중 하나가 인터페이스를 활용해 의존 관계를 분리하는 것이다.

하지만 인터페이스는 메서드에 대한 정의를 다룰 뿐, 데이터 구조에 대한 결합도는 해결하지 못한다.

DTO의 역할: 결합도 낮추기

데이터 구조의 결합도를 낮추는 데 DTO가 필요한 이유는 무엇일까?

만약 Domain 객체의 변수가 변경된다면, 이를 사용하는 다른 메서드들에 반드시 수정이 발생하게 된다. 이렇게 끝없이 수정이 전파되는 상황을 방지하기 위해 DTO를 활용해 Domain과 View 간의 결합도를 낮출 수 있다.

Dto Class

@Getter
@Builder
public class TokenDto {
    private final String accessToken;
    private final String refreshToken;
}

실수와 교훈: DTO에서 @Getter의 중요성

처음 TokenDto를 작성할 때, 테스트를 진행하면서 @Getter를 제외한 적이 있다.

위는 내가 사용했던 TokenDto 클래스 코드이다. 매우 단순한 형태지만, 중요한 역할을 한다.

DTO 사용의 장점

  1. Domain 변경으로 인한 수정 전파 방지
    View는 DTO만 바라보므로, Domain 로직이 변경되어도 View 코드는 수정할 필요가 없다.
  2. 결합도 감소
    Model-Controller와 Controller-View 간의 결합도를 줄여 유지보수가 용이하다.

실수와 교훈: DTO에서 @Getter의 중요성

처음 TokenDto를 작성할 때, 테스트를 진행하면서 @Getter를 제외한 적이 있다.

@Builder
public class TokenDto {
    private final String accessToken;
    private final String refreshToken;
}

테스트 결과, 데이터를 반환하는 과정에서 에러가 발생했다. 원인을 찾아보니 Getter 메서드가 없어 데이터를 읽지 못했기 때문이었다.

왜 DTO에 @Getter가 필수일까?

@RestController를 보면, 다음과 같이 정의되어 있다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

 

여기서 중요한 부분은 바로 @ResponseBody이다.

Spring은 데이터를 반환할 때, 객체를 JSON 형태로 변환해 응답으로 보낸다. 이때 변환 과정을 위해 데이터를 읽어야 하는데, Getter가 없으면 데이터를 읽지 못해 변환에 실패한다. 따라서 DTO 객체를 응답으로 반환할 때, @Getter는 필수적이다.

마무리

DTO를 사용하는 이유는 단순히 데이터를 전송하기 위함이 아니라, Domain 변경으로 인한 수정 전파를 방지하고 결합도를 낮춰 유지보수를 용이하게 하기 위함이다.

또한, DTO를 JSON 응답으로 변환하기 위해 Getter 메서드가 필수적이라는 점도 기억해야 한다. 이번 경험을 통해 DTO 작성 시 @Getter와 같은 어노테이션의 중요성을 다시금 느낄 수 있었다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

+ Recent posts