Spring 숙련주차 -1

dev_joo·2026년 2월 5일

Bean 수동 등록

기술적인 문제나 여러 컴포넌트에서 공통적으로 사용하는 관심사를 처리하는 객체들은 수동으로 Bean을 등록하는 것이 바람직하다.

이러한 객체들은 보통 기술 지원용 Bean으로, 공통 로그 처리와 같이 비즈니스 로직을 직접 담당하지 않으면서도 이를 보조하는 부가적이고 공통적인 기능을 수행한다.

수동으로 등록된 Bean은 문제가 발생했을 때 어디에서 설정되었는지 추적하기 쉽다는 장점이 있어, 의도적으로 수동 등록을 선택하는 경우가 많다.

과거에는 applicationContext.xml을 통해 Bean 등록 설정을 작성했지만,
현재는 Spring Boot를 사용해 이러한 설정을 자동화할 수 있다.

@Bean으로 등록하고자하는 객체를 반환하는 메서드가 속해있는 클래스에 @Configuration 를 추가한다.

Bcrypt: 비밀번호를 암호화해주는 해시함수

같은 타입의 Bean이 여러개 있을때 DI

다음 테스트를 통해 @Autowired가 기본적으로는 Bean Type(Food)으로 DI를 시도하고,
연결이 되지않을 경우 Bean Name(pizza, chicken)으로 찾는다는 것을 알 수 있었다.

@SpringBootTest
public class BeanTest {

    @Autowired
    Food pizza;

    @Autowired
    Food chicken;
    
    /*
    Unsatisfied dependency expressed through field 'food': 
    No qualifying bean of type 'com.sparta.springauth.food.Food' available: 
    expected single matching bean but found 2: chicken,pizza
     */
    @Autowired
    Food food;
    
    @Test
    public void test1(){
        System.out.println("????");
    }
}

@Primary

@Primary가 추가되면 같은 타입의 Bean이 여러 개 있더라도 우선 @Primary가 설정된 Bean 객체를 주입한다.

@Component
@Primary
public class Chicken implements Food {
    @Override
    public void eat() {
        System.out.println("치킨을 먹습니다.");
    }
}
package com.sparta.springauth;

import com.sparta.springauth.food.Food;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class BeanTest {

    @Autowired
    Food pizza;

    @Autowired
    Food chicken;

    @Autowired
    Food food;

    @Test
    public void test1(){
        pizza.eat();
        chicken.eat();
        food.eat();
    }
}

실행 결과:
치킨을 먹습니다.
치킨을 먹습니다.
치킨을 먹습니다.

그런데 실행 결과 pizza도 Chicken 클래스로 주입되었다.

여기서 pizza는 단순 변수명이고,

❌ “pizza Bean을 주세요”가 아니라
✅ “Food 타입 Bean 하나 주세요” 라는 뜻이된다.

따라서 @Primary를 통해 우선 찾게 된 Food 타입의 Bean Chicken이 주입된다.

@Qualifier

이 때, 주입될 구현체를 @Qualifier로 명시하면 해당 객체가 @Primary 객체보다 우선되어 주입된다.

@SpringBootTest
public class BeanTest {


    @Autowired
    Food pizza;

    @Autowired
    Food chicken;

    @Qualifier("pizza")
    @Autowired
    Food food;

    @Test
    public void test1() {
        pizza.eat();
        chicken.eat();
        food.eat(); //(primary는 Chicken)
    }
}

실행결과:
피자를 먹습니다.
치킨을 먹습니다.
치킨을 먹습니다.

그래도 Qualifier보단 Bean Name이 우선이다.

 @Qualifier("pizza")
    @Autowired
    Food food;

첫 번째 Qualifier 예제를 보고, Qualifier가 주입될 구현체를 표시한다고 생각해 다음과 같이 바꿔 테스트해봤다.

@Qualifier("chicken") // <-- ??
@Component
public class Pizza implements Food {
    @Override
    public void eat() {
        System.out.println("피자를 먹습니다.");
    }
}
@Qualifier("pizza") // <-- ??
@Component
public class Chicken implements Food {
    @Override
    public void eat() {
        System.out.println("치킨을 먹습니다.");
    }
}

즉, pizza Bean과 chicken Bean의 Qualifier 데이터를 서로 바꿨다.

그래서 TestCode의 예상은 다음과 같았다.

@SpringBootTest
public class BeanTest {

    @Qualifier("pizza") // @Qualifier("pizza")인 Chicken이 주입될 것
    @Autowired
    Food pizza;

    @Qualifier("chicken") // @Qualifier("chicken")인 Pizza가 주입될 것
    @Autowired
    Food chicken;

    //@Autowired
    //Food food;

    @Test
    public void test1(){
        pizza.eat();
        chicken.eat();
        //food.eat(); (primary는 제거함)
    }
}
예상 결과:
치킨을 먹습니다.
피자를 먹습니다.

그러나 결과는

피자를 먹습니다.
치킨을 먹습니다.

변수명대로??? 나왔다.

그래서 찾아봤다.

주입 받는 부분의 @Qualifier("xxx")의 의미:
-> xxx라는 Bean 이름(변수명 아님!!!)을 우선 찾아라

주입 받는 부분의 @Qualifier("xxx") 처리 순서

1.Bean name == "xxx"
2. 해당 이름의 Bean 없으면 @Qualifier("xxx") 메타데이터를 가진 Bean
3. 그래도 못 찾으면 → 예외

[결론] 같은 타입의 Bean이 여러 개 있을 때 Spring의 주입 기준

Spring은 @Autowired를 통해 의존성을 주입할 때, 다음과 같은 순서로 주입 대상을 결정한다.

  1. 타입으로 Bean 후보를 먼저 조회한다 (Food)

  2. 주입 지점에 @Qualifier가 있으면 이를 최우선으로 적용한다

이때 @Qualifier("xxx")
1️⃣ "xxx"라는 Bean 이름을 먼저 매칭하고
2️⃣ 해당 이름의 Bean이 없을 경우에만
@Qualifier("xxx") 메타데이터를 가진 Bean을 찾는다

  1. @Qualifier가 없고 후보가 여러 개인 경우 → @Primary가 적용된 Bean을 선택한다

  2. 그래도 결정되지 않으면 (Qualifier도 없고, Primary도 없을 때)
    → 변수명과 Bean 이름을 비교해 자동 매칭을 시도한다

  3. 끝내 하나로 결정하지 못하면 → 예외가 발생한다


인증과 인가란 무엇인가?

인증과 인가 개념은 예전 캠프에서 배운적이 있어서 빠르게 넘어갔다.
인증과 인가 세미나 발표 자료

인증(Authentication)

인증은 해당 유저가 실제 유저인지 인증
지문인식, 로그인 등
즉, "나야..." 들기름

인가(Authorization)

인가는 해당 유저가 특정 리소스에 접근이 가능한지 허가
관리자 권한, 글 삭제 권한 등
즉, "나인데?"

웹 개발에서 인증과 인가

HTTP 프로토콜

비연결성(Connectionless)

서버와 클라이언트가 연결되어 있지 않다

무상태(Stateless)

서버가 클라이언트의 상태를 저장하지 않는다

인증의 방식

1. 쿠키-세션 방식의 인증

쿠키-세션 방식은 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식
인증과 관련된 최소한의 정보는 저장해서 로그인을 유지시킨다

쿠키

  • 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
    Name Value Domain Path Expires

  • 쿠키 생성
    Cookie 객체를 생성해 필드를 채우고 HttpServletResponse 객체에 addCookie(cookie); 메서드를 통해 쿠키를 전달한다.

  • public static void addCookie(String cookieValue, HttpServletResponse res) {
            try {
                cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
    
                Cookie cookie = new Cookie("Authorization", cookieValue); // Name-Value
                cookie.setPath("/");
                cookie.setMaxAge(30 * 60);
    
                // Response 객체에 Cookie 추가
                res.addCookie(cookie);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        
  • 쿠키 받기
    Controller 메서드의 파라미터를 @CookieValue("쿠키이름") 로 받는다

  • @GetMapping("/get-cookie")
    public String getCookie(@CookieValue("Authorization") String value) {
        System.out.println("value = " + value);
    
        return "getCookie : " + value;
    }

세션

서버에서 생성한 SESSIONID세션 쿠키 로 클라이언트에 저장되어 서버가 클라이언트를 식별하는데 사용

HttpServletResponse 객체에서 getSession(true/false) HttpSession 객체를 생성/가져오고

HttpSession 객체의 setAtribute("key","value")로 값을 설정, getAttribute("key")로 값을 가져온다.

  • 세션 생성
    req.getSession(true);
    session.setAttribute("Authorization", "value");

  • @GetMapping("/create-session")
    public String createSession(HttpServletRequest req) {
        // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
        HttpSession session = req.getSession(true);
    
        // 세션에 저장될 정보 Name - Value 를 추가합니다.
        session.setAttribute("Authorization", "Robbie Auth");
    
        return "createSession";
    }
  • 브라우저에서는 HttpSession 객체가 알아서 생성한 JSESSIONID 쿠키가 저장되어 보인다.

  • 서버에서는 SESSIONID → 세션 데이터 매핑은 Tomcat 메모리 내부에 org.apache.catalina.session.StandardSession
    StandardManager가 관리하는 Map 같은 구조로 저장된다.

따라서
서버 재시작 → ❌ 세션 전부 소멸
서버 여러 대 → ❌ 세션 공유 안 됨
이 한계를 넘으려면 → Sticky Session / 세션 저장소 / JWT 방식 인증 필요
  • 세션 가져오기

  • req.getSession(false); session.getAttribute("Authorization");

  • @GetMapping("/get-session")
    public String getSession(HttpServletRequest req) {
        // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
        HttpSession session = req.getSession(false);
    
        String value = (String) session.getAttribute("Authorization"); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
        System.out.println("value = " + value);
    
        return "getSession : " + value;
    }

2. JWT 기반 인증

JWT(JSON Web Token)란 인증에 필요한 정보들을 암호화시킨 토큰을 의미한다.
JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별한다.

JWT 방식의 특징

JWT.io 등으로 누구나 복호화가 가능하다. 단, Secret Key 가 없으면 JWT 수정 불가하다.
Header, Payload, Signature 로 이루어진다.
동시 부하가 많을 때 서버의 부하를 낮춰준다.
서버가 별다른 저장소에서 값을 가져오는 대신 JWT Secret 키만 가지고 서버 자체에서 검증 가능

단점으로는 JWT 토큰에 담는 내용이 많아지면 네트워크 부하가 증가한다
Secret 키 보안 부담 (Secret Key 유출 시 JWT 조작 가능)
이미 발급된 여러 JWT 중에서 특정 토큰 하나만 서버에서 즉시 무효화시키는 것이 불가능하다. (Secret Key 로테이션으로 '전체 토큰'을 만료시키기만 가능)

JWT 사용 인증 흐름

  1. Client 가 username, password 로 로그인 성공
  2. 서버에서 "로그인 정보" → JWT 로 암호화 (Secret Key 사용)
  3. 서버에서 JWT를 담은 쿠키를 생성해 Client 응답에 전달
  1. 서버에서 API 요청 시마다 쿠키에 포함된 JWT를 찾음
  2. 찾아낸 JWT 위조 여부 검증 (Secret Key / 유효기간)
  3. 검증 성공시 JWT의 사용자 정보를 가져와 사용
profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글