로컬 캐시와 공유 캐시 

 

로컬 캐시란? 

하나의 프로세스 내부에만 존재하는 캐시 

위치: JVM 메모리, 프로세스 내부 메모리

장점: 접근 속도 가장 빠름

단점: 다른 서버/인스턴스와 절대 공유 안 됨,서버 재시작하면 캐시 전부 날아감

 

공유 캐시란? 

여러 인스턴스가 함께 사용하는 캐시 

위치: 외부 시스템, 네트워크를 통해 접근

장점: 서버 수가 늘어나도 데이터 일관성 유지,로컬 캐시보다 느리지만 DB보단 빠름

단점: 네트워크 장애 영향 받음

 

TTL

데이터가 캐시에 살아있을 수 있는 유효 시간

사용 이유: 캐시는 영원히 믿을 수 있는 저장소가 아니다.

TTL이 없으면 오래된 데이터가 계속 남고 실제 DB 값과 캐시 값 불일치할 수 있으며 메모리가 무한 증가한다. 

그래서 신뢰할 수 있는 기간을 명시하는 용도이다. 

 

TTL이 있어도 공유 캐시를 사용하는 이유 

TTL은 ‘시간 문제’만 해결하고 ‘서버 간 불일치 문제’는 해결하지 못한다.

예를들어 메뉴가 DB에서 품절로 변경되었다고 하더라도 주문 가능한 상태로 사용자에게 보여진다. 

또는 1초 차이로 각 사용자가 보고 있는 메뉴 목록이 다르게 보일 수 있다. 

특히, ttl을 신뢰하고 로컬캐시를 사용하는 것은 정합성을 시간에 맡기는 설계와 같다. 

 

'TIL' 카테고리의 다른 글

TIL/251222  (0) 2025.12.22
TIL/251219  (0) 2025.12.19
TIL/ 연관관계 매핑  (0) 2025.10.21
TIL / RESTful API  (0) 2025.10.17

AuthenticationManager를 bean등록해야하는 이유 


문제 상황

jwt관련하여 발생하는 모든 에러를 403 에러로 반환함 

토큰을 누락하는 경우에도 403, 토큰이 유효하지 않은 경우에도 403 

개발할때 원인을 찾기 어려울뿐더러 클라이언트에서 구분하기 어려움 

고려할점 - 어디까지 에러 코드를 구분할 것인가 (보안차원에서)

 

authenticationEntryPoint

정의

authenticationEntryPoint: 인증되지 않은 사용자가 보호된 리소스에 접근했을 때 무엇을 할지 정하는 시작 지점

인증되지 않은 사용자란

  • 토큰 없음
  • 토큰 만료
  • 로그인 안함 
요청
 ↓
Security Filter Chain // 필터 체인으로 가장 먼저 호출된다 
 ↓
Authentication 확인 // 인증 정보를 확인한다 토큰 등.. 
 ↓
❌ 인증 정보 없음
 ↓
AuthenticationException 발생 // 인증 정보가 없으므로 예외를 발생시킨다 
 ↓
ExceptionTranslationFilter
 ↓
AuthenticationEntryPoint 호출 ✅

 

AuthenticationEntryPoint를 만들지 않으면 모든 에러가 403으로 내려온다 

왜 이런 현상이 발생할까 ?

 

-> Spring Security는 원래 “웹 사이트용” 프레임워크다

그래서 기본 동작이 인증 안되면 로그인 페이지로 다시 보내도록 기본 설정이 되어있다

그럼 여기서 AuthenticationEntryPoint를 만들지 않았다면 기본 설정대로 LoginUrlAuthenticationEntryPoint를 등록한다 

이 EntryPoint의 역할은 로그인 페이지로 리다이렉트 시키는 일을 한다 

 

전혀문제될게 없어보이지만 api를 구현한다면 보통 json으로 데이터를 주고 받을 것이라고 예상하지만 

스프링 시큐리티는 로그인 페이지로 응답할 것이다 (기본 설정 때문에) 

더보기
더보기

//json을 예상했지만 실제 스프링이 응답으로 보낸것 

 

<html>
  <form>login</form>
</html>

그러면 파싱에러가 날것이다 기대한 타입과 다르므로 

 

따라서 내가 커스텀하게 EntryPoint을 바꾸는것이 authenticationEntryPoint을 사용하는 것과 같다 

 

주의할점

AuthenticationEntryPoint는 Security Filter Chain 안에서 AuthenticationException이 발생했을 때만 호출된다

 

즉 다른 에러를 사용해서 다음과 같이 코드가 작성되어 있었기 때문에 인식을 못했다.

RuntimeException / IllegalStateException는 EntryPoint를 호출하지 않는다 

try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (JwtException | IllegalArgumentException e) {
        log.warn("잘못된 JWT 토큰: {}", e.getMessage());
        return false;
    }

 

의문점 1 - EntryPoint 설정을 추가하지 않고 jwt를 예외처리하면 안되나? 

AuthenticationEntryPoint가 필요한 이유는 무엇일까 

예외처리는 기본적으로 호출한 곳의 상위로 throws 예외를 던지기 때문에 globalException에서 처리할 수 없다

jwt필터는 인증 보다 ,컨트롤러 호출 보다도 앞에서 호출된다.

JwtAuthenticationFilter (OncePerRequestFilter)
  ↓
UsernamePasswordAuthenticationFilter
  ↓
AuthorizationFilter
  ↓
ExceptionTranslationFilter

 

여기서 살펴볼 것은 예외처리란 무엇인가이다. 

숫자를 0으로 나눌때와 같은 예외를 들어보자

이는 “이 입력은 내 책임 범위 밖이다 호출자가 처리해라”를 명시하면서 ArithmeticException를 던진다 

 

그런데 jwt를 보자 null,만료,위조 모두 시스템 오류가 아니다. 

다시말해 실패 != 예외임을 헷갈려선 안된다 

jwt를 검증할 수 없다 == 보안 판단을 할 수 없다 

jwt를 검증할 수 없다 != 심각한 오류가 발생했다 

parserBuilder도중 실패 = 라이브러리 오류 = 심각한 오류 발생 = 예외처리 필요

 

즉 스프링 시큐리티 입장에서 jwt검증 실패는 처리할 수 있는 정상 범위니까 예외가 아니라 판단해야 하는 상태라고 본다 

 

의문점 2 - EntryPoint 설정을 추가하지 않고 jwt에서 AuthenticationException을 던지면 되지 않을까? 

필터에서 인증 실패가 발생하면, 바로 다음 단계로 넘어가지 않고 ExceptionTranslationFilter가 잡음

클라이언트 요청 →
  SecurityContextPersistenceFilter →
  JwtFilter / UsernamePasswordAuthenticationFilter →
  ExceptionTranslationFilter →
  기타 인증/인가 필터 →
  DispatcherServlet →
  Controller

 

그리고 AuthenticationManager 내부에서 발생한 에러만 ExceptionTranslationFilter가 catch 

AuthenticationManager authManager = ...;
try {
    authManager.authenticate(token); // <- 이 라인에서 실패하면 AuthenticationException 발생
} catch (AuthenticationException ex) {
    // Security는 이걸 인증 실패로 간주
}

 

ExceptionTranslationFilter의 핵심 로직

try {
    filterChain.doFilter(request, response);  // 다음 필터로 진행
} catch (AuthenticationException ex) {
    authenticationEntryPoint.commence(request, response, ex);  // 인증 실패 처리
} catch (AccessDeniedException ex) {
    accessDeniedHandler.handle(request, response, ex);           // 권한 없음 처리
}

 

필터에서 직접 던지면:

  • filterChain.doFilter() 바깥에서 RuntimeException 발생
  • ExceptionTranslationFilter는 try 안에서 발생한 인증 시도 실패만 보고 catch
  • 그래서 Security 입장에서는 “내가 만든 인증 실패가 아니다” → 그냥 RuntimeException
  • 결과: EntryPoint 호출 안 됨

어째서 바깥에서 발생했는가? 

말 그대로 doFilter를 호출하기 전에 jwt검증을 끝냈다 

그래서 이게 왜 바깥이냐? doFilter 호출로 try-catch를 하기 때문임

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
    FilterChain filterChain) throws ServletException, IOException {

    String jwt = resolveToken(request);

    if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
        Authentication authentication = jwtTokenProvider.getAuthentication(jwt);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    filterChain.doFilter(request, response);
}

 

결론

Spring Security에서 인증 실패를 자동으로 EntryPoint로 연결시키려면 인증 시도가 반드시 AuthenticationManager를 거쳐야 함.

 

 

 

AccessDeniedHandler: 로그인 했지만(=인증 통과) 권한 부족한 경우

 

 

 

'TIL' 카테고리의 다른 글

TIL/ 260112  (0) 2026.01.13
TIL/251219  (0) 2025.12.19
TIL/ 연관관계 매핑  (0) 2025.10.21
TIL / RESTful API  (0) 2025.10.17

테스트 코드를 왜 써야하는가  

잘못된 인식

  • 테스트 = 귀찮은 추가 작업
  • 테스트 = 커버리지 숫자 맞추기
  • 테스트 = 모든 메서드에 assert 붙이기

올바른 인식

  • 테스트는 설계의 결과물
  • 테스트는 의도를 문서화하는 코드
  • 테스트는 미래의 나(또는 팀원)를 살리는 보험

=> 테스트가 쓰기 어렵다면, 설계가 잘못된 경우가 많다

테스트는 책임 단위로 작성

예시:

  • UserService
    • 모든 기능 다 테스트
    • 회원 가입 규칙을 올바르게 처리하는가?

즉, 테스트 이름만 봐도 이 코드는 무엇을 책임지는지 보여야 한다.

 

주의할점 

Controller 테스트는 Service를 신뢰해야 한다

Service 동작까지 검증하면  중복 테스트 + 책임 침범으로 본다 

따라서 약속된 HTTP 상태 코드가 내려오는지만 검증한다 

 

정상 흐름 vs 예외 흐름

구분 설명
Happy Path 정상 입력 → 정상 결과
Edge Case 경계값, null, 빈 값
Error Case 예외 발생, 잘못된 요청

 

정상 흐름이란 : 시스템이 기대하는 조건을 모두 만족했을 때, 의도한 결과가 나오는 경로

@Test
void 회원가입이_정상적으로_성공한다() {
    // given
    SignupRequest req = validRequest();

    // when
    User user = authService.signup(req);

    // then
    assertThat(user.getId()).isNotNull();
}

 

이 테스트의 역할

  • 기본 기능이 살아있는지 확인
  • 리팩토링 후 “최소한 이건 깨지지 않았는지” 보장

하지만 이것만 있으면 테스트의 30%만 한 것이다.

 

예외 흐름이란: 

현실의 입력은 대부분 “정상”이 아님을 생각해야한다.

사용자가 잘못된 값을 입력하거나 네트워크 상태가 끊긴다거나 등.. 이러한 잘못된 상황을 막는것이 백엔드가 해야할일!

 

예외 흐름 종류 1 - 검증 실패 

입력값 자체가 잘못된 경우를 말한다 

 

  • 이메일 형식 오류
  • 비밀번호 길이 부족

 

예외 흐름 종류 2 - 상태 기반 예외

입력은 정상인데, 현재 상태가 허용하지 않는 경우를 말한다 

 

  • 이미 탈퇴한 유저
  • 이미 결제 완료된 주문

예외 흐름 종류 3 - 상태 기반 예외

외부 요인, 인프라 문제를 말한다. 

  • DB저장 실패시 회원가입 롤백되는가 
  • 외부 API 장애

 

이 테스트는 “왜 존재하는가?”

모든 테스트는 질문 하나에 답해야 한다.

“이 테스트가 없으면 어떤 버그를 못 잡을까?”

이 질문에 답 못 하면 의미 없는 테스트일 확률이 높다

 

Mock을 쓴다는 것의 진짜 의미

Mock은 “편의”가 아니라 “경계 설정”이다

Mock을 쓴다는 말은 곧:

 

“나는 이 객체의 내부 구현이 아니라, 역할만 신뢰한다”

 

예시:

  • Service 테스트에서
    • Repository는 Mock
    • 이유: DB 검증이 목적이 아니기 때문

Mock 남용 = 테스트 신뢰도 하락
Mock 부족 = 테스트가 느리고 불안정

 

테스트는 설계 리뷰 도구다

테스트 작성 중 느끼는 감정 의미
세팅이 너무 길다 책임이 많다
Mock이 너무 많다 결합도가 높다
테스트 이름 짓기 어렵다 역할이 모호하다
private 메서드 테스트하고 싶다 구조가 잘못됐다

 

 


JSON

 

json <->Dto 변환시

json에서는 권한을 string으로 전달하더라도

자바 dto에서는 이를 enum타입으로 명시했다면 자동으로 변환해준다 

 

그렇다면 테스트에서 dto-> json으로 변환하는 과정을 거치는 이유가 무엇일까? 

1.dto를 수정했을 때 런타임이 아닌 컴파일 단에서 에러를 잡을 수 있다 

2.json으로 작성했을 경우 까먹고 넘어갈 수 있다 

테스트는 통과할 수도 있음 (실제 API는 깨짐)

 

mockMvc: 가짜 HTTP 요청 보내기

  • 실제 서버를 띄우지 않고
  • Spring MVC DispatcherServlet까지만 실행하는 테스트 도구
mockMvc.perform(post("/api/auth/signup")

 

 

HTTP 헤더 설정

Spring은 이걸 보고:

  • @RequestBody를 무엇으로 파싱할지 결정
  • 여기선 JSON으로 파싱
 .contentType(MediaType.APPLICATION_JSON)

 

DTO 객체 → JSON 문자열 변환 후 HTTP Body에 JSON을 담음

.content(objectMapper.writeValueAsString(signupReqDto)))

 

 

http 요청 결과 형태

POST /api/signup
Content-Type: application/json

{
  "email": "test@test.com",
  "password": "password123",
  "role": "BUYER"
}

 

@MockBean

Spring Boot 3.4.0 버전부터 @MockBean Deprecated

@MockitoBean으로 대체

 

 

 

엄청나게 긴 오류코드를 마주했다 한줄씩 해석해보자

***************************

APPLICATION FAILED TO START

***************************



Description:



Parameter 0 of constructor in com.example.yongeunmarket.jwt.JwtFilter required 

a bean of type 'com.example.yongeunmarket.jwt.JwtTokenProvider' that could not be found.



2025-12-19T17:42:45.669+09:00  WARN 2960 --- [yongeun-market] [    Test worker] o.s.test.context.TestContextManager      : Caught exception while allowing TestExecutionListener [org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener] to prepare test instance [com.example.yongeunmarket.controller.AuthControllerTest@5db0003d]



java.lang.IllegalStateException: Failed to load ApplicationContext for

[WebMergedContextConfiguration ...

org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTestContextBootstrapper=true]



Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtFilter' defined in file [/Users/heoarim/sparta/consumer-to-consumer-project/build/classes/java/main/com/example/yongeunmarket/jwt/JwtFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.example.yongeunmarket.jwt.JwtTokenProvider' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804) ~[spring-beans-6.2.14.jar:6.2.14]



... 생략
  • JwtFilter 클래스가 문제다 JwtTokenProvider 주입받으려다 실패했다 왜? Bean이 없다
  • 스프링의 핵심 컨테이너인 ApplicationContext를 로드하는데 실패했다 

여기서 알아야할 것은 컨트롤러 테스트를 하고있는데 스프링을 띄우다가 실패했다는 것이다

'TIL' 카테고리의 다른 글

TIL/ 260112  (0) 2026.01.13
TIL/251222  (0) 2025.12.22
TIL/ 연관관계 매핑  (0) 2025.10.21
TIL / RESTful API  (0) 2025.10.17

연관관계 매핑이란 객체 지향 프로그래밍에서 엔티티 객체 간의 관계를 관계형 데이터베이스의 외래 키 관계와 연결하는 과정을 말합니다. 

 

1:N 연관

흔한 예로는 부서와 직원 간의 관계가 있습니다 . 각 부서에는 여러 직원 이 있지만, 각 직원은 하나의 부서에만 속합니다 .

@Entity
public class Department {
 
    @Id
    private Long id;
 
    @OneToMany
    @JoinColumn(name = "department_id")
    private List<Employee> employees;
}

@Entity
public class Employee {
 
    @Id
    private Long id;
}

 

 

N:1 연관

다대일 관계에서는 엔터티의 여러 인스턴스가 다른 엔터티의 한 인스턴스와 연결됩니다 .

예를 들어  학생은 하나의 학교 에만 등록할 수 있지만 , 각 학교는  여러 학생 을 가질 수 있습니다 .

 

다대일 단방향 연관 예시

@ManyToOne 어노테이션을 사용한다. 

@JoinColumn 어노테이션은 Student 엔터티  School 엔터티를 조인할 외래 키 열 이름을 지정합니다  .

@Entity
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "school_id")
    private School school;
}

@Entity
public class School {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

 

다대다 관계

다대다 관계 에서는 한 엔터티의 여러 인스턴스가 다른 엔터티의 여러 인스턴스와 연결됩니다 .

예시) Book은 여러 Author 를 가질 수 있고 , 각 Author는 여러 Book 을 작성할 수 있습니다.

JPA에서 이 관계는 @ManyToMany 어노테이션을 사용하여 표현됩니다.

 

다대다 관계 예시

  • @ManyToMany 어노테이션은 각 Book이 여러 Author 를 가질 수 있고 , 각 Author가 여러 Book 을 작성할 수 있음을 지정
  • @JoinTable 어노테이션은 Book 과 Author 엔터티를 조인할 조인 테이블의 이름과 외래 키 열을 지정합니다.
@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToMany
    @JoinTable(name = "book_author",
            joinColumns = @JoinColumn(name = "book_id"),
            inverseJoinColumns = @JoinColumn(name = "author_id"))
    private Set<Author> authors;

}

@Entity
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

'TIL' 카테고리의 다른 글

TIL/ 260112  (0) 2026.01.13
TIL/251222  (0) 2025.12.22
TIL/251219  (0) 2025.12.19
TIL / RESTful API  (0) 2025.10.17

📅 Today I Learned

오늘은 RESTful API를 공부하면서, 그동안 내가 ‘표면적으로만’ 이해하고 있던 부분을 바로잡을 수 있었다.
특히, HTTP 메서드가 핵심이라고 착각했던 부분을 구체적인 코드와 함께 정리했다.


🚧 내가 처음 작성한 코드

이 코드를 작성하면서 그냥 http매서드를 목적에 맞게 사용하는 것이라고만 생각했다. 즉, 데이터를 조회할 때는 GET, 수정할 때는 PUT 정도의 규칙만 잘 지키면 RESTful API가 된다고 생각했다. 

@GetMapping("/schedules/{author}")
public ResponseEntity<List<GetOneScheduleResponse>> getAllSchedule(@PathVariable String author) {
    List<GetOneScheduleResponse> all = scheduleService.getAll(author);
    return ResponseEntity.status(HttpStatus.OK).body(all);
}

@PutMapping("/schedules/{userId}")
public ResponseEntity<UpdateScheduleResponse> updateSchedule(@PathVariable Long userId, @RequestBody UpdateScheduleRequest updateScheduleRequest) {
    UpdateScheduleResponse update = scheduleService.update(userId, updateScheduleRequest);
    return ResponseEntity.status(HttpStatus.OK).body(update);
}

💡 RESTful의 핵심은 HTTP 메서드가 아니다!

  • RESTful URL은 “무엇(자원)”을 표현하는 명사 중심의 구조이며 “어떻게(행위)”는 HTTP 메서드로 구분한다.
    URL은 ‘데이터의 위치’를 나타내야지, ‘행동’을 표현하면 안 된다.
  • 헷갈렸던 URL규칙 
    • 계층 구조를 이용해 관계 표현: /users/1/orders/5 (1번 유저의 5번 주문)
    • 필터링이나 정렬은 쿼리 파라미터로: /products?category=phone&sort=price
    • 리소스 식별은 Path Variable로: /users/{userId}

🧠 정리

RESTful API는 단순히 HTTP 메서드를 쓰는 규칙이 아니라,

리소스를 중심으로 URI를 설계하고, 그 리소스에 대한 행위를 HTTP 메서드로 표현하는 구조적 원칙이다.

 

 

'TIL' 카테고리의 다른 글

TIL/ 260112  (0) 2026.01.13
TIL/251222  (0) 2025.12.22
TIL/251219  (0) 2025.12.19
TIL/ 연관관계 매핑  (0) 2025.10.21

+ Recent posts