[TIL] 스프링 숙련 - Bean 수동 등록과 인증
Spring에서 Bean을 수동 등록하는 상황과 자동 등록과의 차이를 정리한 TIL입니다.
For the English version of this post, see here.
[TIL] 스프링 숙련 - Bean 수동 등록과 인증
오늘 할 일
공부한 내용
Bean 수동으로 등록하기
Bean 수동 등록
- Bean 자동 등록:
@Component를 사용하면@ComponentScan에 의해 자동으로 스캔되어 해당 클래스를 Bean으로 등록해줌- 일반적으로 위처럼 자동 등록함
- 프로젝트의 규모가 커질 수록 등록할 Bean들이 만아지기 때문에 자동등록을 사용하면 편리함
- 비즈니스 로직과 관련된 클래스들은 그 수가 많기 때문에
@Controller,@Service와 같은 어노테이션을 사용해 Bean으로 등록하고 관리하여 개발 생산성을 유리하게 함
Bean 수동 등록은 언제?
- 기술적인 문제나 공통적인 관심사를 처리할 때 사용하는 객체들은 수동으로 등록하는 것이 좋음
- 기술 지원 Bean: 공통 로그처리와 같은 비즈니스 로직을 지원하기 위한 부가적이고 공통적인 기능들
- 비즈니스 로직 Bean보다는 그 수가 적기 때문에 수동으로 등록하기 부담스럽지 않음
- 수동 등록된 Bean에서 문제가 발생했을 때, 해당 위치를 파악하기 쉬움
Bean 수동 등록 방법
1 2 3 4 5 6 7 8
@Configuration public class PasswordConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
- Bean으로 등록하고자하는 객체를 반환하는 메서드를 선언하고
@Bean을 설정함 - Bean을 등록하는 메서드가 속한 해당 클래스에
@Configuration을 설정함 - Spring 서버가 뜰 때 Spring IoC 컨테이너에 ‘Bean’으로 저장됨
- ‘Bean’ 이름:
@Bean이 설정된 메서드명 ex)public PasswordEncoder passwordEncoder() {..}-> passwordEncoder
- ‘Bean’ 이름:
같은 타입의 Bean이 2개라면
- Chicken
1 2 3 4 5 6 7 8 9 10 11 12
package com.sparta.springauth.food; import org.springframework.stereotype.Component; @Component @Primary public class Chicken implements Food { @Override public void eat() { System.out.println("치킨을 먹습니다."); } }- @Primary: 같은 타입의 Bean이 여러 개 있더라도 우선 @Primary가 설정된 Bean 객체를 주입해줌
- Pizza
1 2 3 4 5 6 7 8 9 10 11 12 13
package com.sparta.springauth.food; import org.springframework.stereotype.Component; @Component @Qualifier("pizza") public class Pizza implements Food { @Override public void eat() { System.out.println("피자를 먹습니다."); } }
- Pizza 클래스에
@Qualifier("pizza")를 추가함 - 주입하고자 하는 필드에도(test의 필드) `@Qualifier(“pizza”)를 추가해주면 해당 Bean 객체가 주입됨
- Pizza 클래스에
- Food Interface
1 2 3 4 5
package com.sparta.springauth.food; public interface Food { void eat(); }
- Food 타입의 Bean 객체 Chicken, Pizza를 등록함
- test 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@SpringBootTest public class BeanTest { @Autowired @Qualifier("pizza") Food food; @Test @DisplayName("Primary 와 Qualifier 우선순위 확인") void test1() { // 현재 Chicken 은 Primary 가 적용된 상태 // Pizza는 Qualifier 가 추가된 상태입니다. food.eat(); } }@Autowired Food food;처럼 사용한다면 food 필드에 Bean을 주입해줘야하는데 같은 타입의 Bean 객체가 하나 이상(chicken, pizza)이기 때문에 어떤 Bean을 등록해줘야할지 몰라 오류가 발생함- 등록된 Bean 이름 명시하여 해결 가능
@Autowired Food pizza;,@Autowired Food chicken;@Autowired가 기본적으로 Bean Type(Food)로 DI를 지원하며, 연결이 되지 않을 경우 Bean Name(chicken, pizza)으로 찾는다는 것을 알 수 있음
- 등록된 Bean 이름 명시하여 해결 가능
- 같은 타입의 Bean들에 Qualifier와 Primary가 동시에 적용되어있다면 Qualifier의 우선순위가 더 높음
- 하지만, Qualifier는 적용하기 위해 주입 받고자하는 곳에 해당 Qualifier를 반드시 추가해야함 -> 따라서 같은 타입의 Bean이 여러 개 있을 때는 범용적으로 사용되는 Bean 객체에는 Primary를 설정하고 지엽적으로 사용되는 Bean 객체에는 Qualifier를 사용하는 것이 좋음
인증과 인가
- 인증 (Authentication)
- 해당 유저가 실제 유저인지 인증하는 개념 인가 (Authorization)
- 해당 유저가 특정 리소스에 접근이 가능한지 허가를 확인하는 개념
웹 어플리케이션 인증의 특수성
- 일반적으로 서버-클라이언트 구조로 되어있고, 실제로 이 두가지 요소는 아주 멀리 떨어져있음
- Http라는 프로토콜을 이용해여 통신하는데, 그 통신은 비연결성(Connectionless) 무상태(Stateless)로 이루어짐
- 비연결성 (Connectionless): 서버와 클라이언트가 연결되어 있지 않다는 것
- 리소스를 절약하기 위해
- 서버와 클라이언트가 실제로 계속 연결되어있다면, 서버의 비용이 기하급수적으로 늘어남 -> 그래서 서버는 실제로 하나의 응답을 내면, 연결을 끊어버림 - 무상태 (Stateless): 서버가 클라이언트의 상태를 저장하지 않음
- 기존의 상태를 저장하는 것들도 마찬가지로 서버의 비용과 부담을 증가시키는 것이기 때문에, 기존의 상태가 없다고 가정하는 프로토콜을 이용해 구현되어있음 -> 실제로 서버는 클라이언트가 전에 어떤 요청을 보냈는지 전혀 알지 못함
인증의 방식
- 일반적으로 웹 어플리케이션은 두 가지 방법을 통해 인증을 처리함
- 쿠키-세션 방식의 인증
- 사용자가 로그인 요청을 보냄
- 서버는 DB의 유저 테이블에서 아이디 비밀번호를 대조해봄
- 실제 유저테이블의 정보와 일치한다면, 인증을 통과한 것으로 보고 “세션 저장소”에 해당 유저가 로그인 되었다는 정보를 넣음
- 세션 저장소에서는 유저의 정보와는 관련 없는 난수인 session-id를 발급함
- 서버는 로그인 요청의 응답으로 seesion-id를 내어줌
- 클라이언트는 그 session-id를 쿠키라는 저장소에 보관하고 앞으로의 요청마다 세션 아이디를 같이 보냄 (주로 HTTP header에 담아 보냄)
- 클라이언트의 요청에서 쿠키를 발견했다면, 서버는 세션 저장소에서 쿠키를 검증함
- 만약 유저 정보를 받아왔다면 -> 이 사용자는 로그인이 되어있는 사용자
- 이후에는 로그인 된 유저에 따른 응답을 내어줌
- JWT 기반 인증
- 사용자가 로그인 요청을 보냄
- 서버는 DB의 유저 테이블을 뒤져서 아이디 비밀번호를 대조해봄
- 실제 유저테이블의 정보와 일치한다면, 인증을 통과한 것으로 보고 유저의 정보를 JWT로 암호화해서 내보냄
- 서버는 로그인 요청의 응답으로 jwt 토큰을 내어줌
- 클라이언트는 그 토큰을 저장소에 보관하고 앞으로의 요청마다 토큰을 같이 보냄
- 클라이언트의 요청에서 토큰을 발견했다면 서버는 토큰을 검증함
- 이후에는 로그인 된 유저에 따른 응답을 내줌
쿠키와 세션
- 쿠키와 세션 모두 HTTP에 상태 정보를 유지(Stateful)하기 위해 사용됨
- 쿠키와 세션을 통해 서버에서는 클라이언트 별로 인증 및 인가를 할 수 있게됨
- 쿠키
- 클라이언트에 저장될 목적으로 생성한 작은 정보를 담은 파일
- 개발자도구 > Application > Storage > Cookies
- 구성요소:
- Name (이름): 쿠키를 구별하는 데 사용되는 키
- Value (값): 쿠키의 값
- Domain (도메인): 쿠키가 저장된 도메인
- Path (경로): 쿠키가 사용되는 경로
- Expires (만료기한): 쿠키의 만료기한 (만료기한이 지나면 삭제됨)
- 세션
- 서버에서 일정시간 동안 클라이언트 상태를 유지하기 위해 사용됨
- 서버에서 클라이언트 별로 유일무이한 ‘세션 ID’를 부여한 후 클라이언트 별 필요한 정보를 서버에 저장함
- 서버에서 생성한 ‘세션 ID’는 클라이언트의 쿠키값(‘세션 쿠키’ 라고 부름)으로 저장되어 클라이언트 식별에 사용됨
- 세션 동작 방식

- 클라이언트가 서버에 1번 요청
- 서버가 세션ID를 생성하고, 쿠키에 담아 응답 헤더에 전달 - 세션 ID 형태: “SESSIONID = 12345”
- 클라이언트가 쿠키에 세션ID를 저장 (‘세션 쿠키’)
- 클라이언트가 서버에 2번 요청 - 쿠키값 (세션 ID) 포함하여 요청
- 서버가 세션ID를 확인하고, 1번 요청과 같은 클라이언트임을 인지
- 쿠키
쿠키 다루기
- 쿠키 생성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public static void addCookie(String cookieValue, HttpServletResponse res) { try { cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행 // cookieValue 값에 있는 공백을 없애주는 코드 Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value cookie.setPath("/"); cookie.setMaxAge(30 * 60); // Response 객체에 Cookie 추가 res.addCookie(cookie); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e.getMessage()); } }
new Cookie(AUTHORIZATION_HEADER, cookieValue);: Cookie에 저장될 Name과 Value를 생성자로 받는 Cookie 객체를 생성setPath("/"),setMaxAge(30 * 60): Path와 만료시간을 지정함- HttpServletResponse 객체에 생성한 Cookie 객체를 추가하여 브라우저로 반환함
- 이렇게 반환한 Cookie는 브라우저의 Cookie 저장소에 저장됨
Cookie 생성은 범용적으로 사용될 수 있기 때문에 static 메서드로 선언함
- 쿠키 읽기
1 2 3 4 5 6
@GetMapping("/get-cookie") public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) { System.out.println("value = " + value); return "getCookie : " + value; }
@CookieValue("Cookie의 Name"): Cookie의 Name 정보를 전달해주면 해당 정보를 토대로 Cookie의 Value를 가져옴
세션 다루기
- Servlet에서는 유일무이한 ‘세션 ID’를 간편하게 만들 수 있는 HttpSession을 제공해줌
- HttpSession 생성
1 2 3 4 5 6 7 8 9 10
@GetMapping("/create-session") public String createSession(HttpServletRequest req) { // 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환 HttpSession session = req.getSession(true); // 세션에 저장될 정보 Name - Value 를 추가합니다. session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth"); return "createSession"; }
- HttpServletRequest를 사용하여 세션을 생성 및 반환할 수 있음
req.getSession(true): 세션이 존재할 경우 세션을 반환하고 없을 경우 새로운 세션을 생성함- 세션에 저장할 정보를 Name-Value 형식으로 추가함
- 세션은 빈 공간일 뿐이기 때문에, 우리가 원하는 정보(로그인 정보, 사용자 정보 등)은 직접 넣어줘야함
- session = req.getSession(true)는 요청 보낸 사용자에 대해 그 사용자에 대한 세션이 존재하는 지 확인하고, session.setAttribute(AUTHORIZATION_HEADER, “Robbie Auth”)은 이전 코드로 인해 새로 생성되거나 이미 존재하는 세션 안에 데이터를 넣어주는 것
- 반환된 세션은 브라우저 Cookie 저장소에 ‘JSESSIONID’라는 Name으로 Value에 저장됨
- HttpSession 읽기
1 2 3 4 5 6 7 8 9 10
@GetMapping("/get-session") public String getSession(HttpServletRequest req) { // 세션이 존재할 경우 세션 반환, 없을 경우 null 반환 HttpSession session = req.getSession(false); String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다. System.out.println("value = " + value); return "getSession : " + value; }
req.getSession(false): 세션이 존재할 경우 세션을 반환하고 없을 경우 null을 반환함session.getAttribute("세션에 저장된 정보 Name"): Name을 사용하여 세션에 저장된 Value를 가져옴
문제 & 오류
/api/create-cookie 호출 실패
- 문제: 컨트롤러까지 요청이 도달하지 못함
- 원인:
implementation 'org.springframework.boot:spring-boot-starter-security'의존성- 위 의존성으로 인해 Spring Security가 동작
- 모든 요청 보호, 로그인 안 되어 있으면 인증 요구,
/login기본 로그인 페이지 제공 -> 따라서 위 요청을 하면, Spring Security 필터가 먼저 인증이 됐는지 확인 -> 안 되어있으면/login으로 리다이렉트 -> 기본 로그인 페이지 표시됨
- 해결: 내가 가진 Spring Boot 4 내부에서는
exclude = SecurityAutoConfiguration.class를 통해서 시큐리티 자동설정의 상태를 끄려고 해도 그 종류가 많기 때문에, 위 코드처럼 하나만 제외해도 나머지는 살아있어서 오류가 발생 -> 따라서, main > java > config > SecurityConfig.java에 ``` java package com.sparta.springauth.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain;
@Configuration public class SecurityConfig {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.formLogin(form -> form.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/create-cookie",
"/api/get-cookie",
"/api/create-session",
"/api/get-session"
).permitAll()
.anyRequest().authenticated()
);
return http.build();
} } ``` 위 코드를 통해서 해당 경로로 이동할 때, 기본 로그인 페이지(`/login`)를 안 뜨게 하고, 쿠키 테스트할 때 방해되는 csrf도 끔


