Software Release Life Cycle(SRLC)는 소프트웨어를 개발, 테스트 및 배포하는 과정이다. SRLC에는 Pre-alpha, Alpha, Beta, Release Candidate, RTM, GA, Gold 등 여러 단계가 있지만 크게는 소프트웨어를 개발 및 테스트하는 단계와 배포하는 단계로 나눌 수 있다.

 

Pre-Alpha

 

SRLC의 첫 단계로, 직접적인 개발을 하기 전 필요한 사항들을 분석한다. 예를 들면 소비자의 요구사항 파악, 필요한 개발 기능 파악이 있다.

 

 

Alpha

 

alpha 배포 단계에서는 앞 단계에서 정의한 문제를 해결하는 잠재적 솔루션(소프트웨어)을 만든다. 프로토타입을 제작하는 것과 같다. 오픈소스가 아닌 이상 대중에게 프로덕트를 공개하지는 않으며, 프로덕트도 서비스의 모든 기능이 추가된 상태는 아니다.

 

서비스의 일부 기능을 구현하였으나 버그가 있을 수 있으며 데이터 손실 등의 버그도 있을 수 있다. 즉 실제 배포에서 일어나서는 안 되는 종류의 버그들이 있을 수 있다. 그래서 내부적으로 테스트 과정을 거친다.

 

 

Beta

 

pre-alpha에서 기획했던 기능은 다 완성된 상태이나 여전히 버그 가능성은 있다. alpha에서 언급한 데이터 손실, 충돌 등의 문제가 여전히 발생할 수 있다. Beta 단계에서의 목표는 사용성 테스트(usability test)를 통해 실제 유저가 서비스를 사용할 수 있도록 하는 것이다. Beta 단계부터는 테스터를 모집하고, 정식 출시를 하는 소프트웨어도 있다. 인터넷 같은 일부 서비스는 beta 단계에서 계속 머무르기도 한다.

 

 

Release Candidate

 

stable release 버전이 될 가능성이 있는 beta 버전이다. 모든 기능들이 개발된 버전이다. beta 버전 사이클을 1번 이상 거치고, 중대한 버그(데이터 손실, 충돌 등)가 발생하지 않은 상태이지만 자잘한 버그의 수정은 있을 수 있다. 보통 이 단계까지를 개발 단계로 보고, 그 다음부터는 배포 단계로 본다.

 

 

GA(General Availability)

 

프로덕트가 상업적으로 이용 가능한 상태가 된다. 이 단계에서도 프로덕트가 수정될 수는 있는데, 버그 수정 등의 오류가 아니라 더 상품성을 높이기 위해서 수정된다. 예를 들면 보안 기능이나 여러 지역에 서비스를 배포하는 것 등이 해당한다.

 

 

Production Release(Stable Release)

 

정식 배포가 된 상태이다.

 

 

참고한 포스트

https://ausdto.github.io/service-handbook/alpha/1-introduction/1-1-what.html

https://en.wikipedia.org/wiki/Software_release_life_cycle

https://theproductmanager.com/topics/software-release-life-cycle/

'server-side > server' 카테고리의 다른 글

웹 어플리케이션 서버(WAS)  (0) 2024.08.05
Mac 환경설정  (0) 2024.07.15
OAuth 2.0 기본원리  (0) 2022.09.26
인증(Authentication)  (0) 2022.07.14
linux: cron 사용해서 자동으로 스케줄 실행하기  (0) 2022.07.09

poetry는 파이썬에서 패키지를 관리하는 도구이다. 이전에는 requirements.txt, setup.py 등 여러 파일에 의존성 관련 정보를 두고 사용했는데 poetry를 사용하면 pyproject.toml 파일 하나로 의존성을 관리할 수 있다. 또한 기존 requirements.txt를 이용한 방식은 pip라는 패키지 관리 프로그램을 같이 사용해야 했지만, poetry는 다른 패키지 관리 프로그램이나 파일을 더 사용하지 않고 poetry 가상환경을 따로 만들어서 관리할 수 있다.

 

poetry 환경 설정하기

우선은 poetry를 설치하는 방법부터 알아보자.

curl -sSL <https://install.python-poetry.org> | python3 -

 

기존에는 다른 url이었는데 바뀐 듯 하다.

이 명령어를 실행하기 전에 poetry 공식문서를 보고 명령어가 바뀐 게 있는지를 참고하면 좋을 것 같다.

poetry --version

 

터미널에 검색했을 때 버전이 정상적으로 나온다면 잘 설치된 것이다.

 

poetry를 실행하려면 pyproject.toml 파일을 작성해야 한다. 해당 문서를 참고하면 좋을 것 같다.

pyproject.toml 파일에 의존성 패키지 리스트를 작성할 때는 []를 사용해서 여러 상황마다 사용할 패키지를 다르게 설정할 수 있다. 기본값은 [tool.poetry]이고 이 뒤에 조건을 추가한다.

 

예를 들어 테스트 환경에서만 필요한 패키지들이 있다면 다음과 같이 적어주면 된다.

[tool.poetry.group.test]  # This part can be left out

[tool.poetry.group.test.dependencies]
pytest = "^6.0.0"
pytest-mock = "*"

 

상황이나 환경별로 의존성 패키지들을 따로 적을 필요가 없다면, 보통은 [tool.poetry.dependencies]로 선언하고 그 아래 필요한 패키지들을 적는다.

[tool.poetry.dependencies]  # main dependency group
httpx = "*"
pendulum = "*"
[tool.poetry.group.test.dependencies]
pytest = "^6.0.0"
pytest-mock = "*"

 

만약 실제 서버 환경과 배포 환경이 나눠져 있다면 [tool.poetry.dev.dependencies]로 선언하면 된다.

 

poetry 가상환경 만들기

앞서 poetry에서 가상환경을 만들 수 있다고 했었다. poetry 가상환경을 만들고 나면 pyproject.toml 파일의 의존성과 poetry 가상환경의 패키지들을 명령어로 동기화시킬 수 있다.

poetry shell

 

그러면 poetry 가상환경이 터미널에서 실행되고, 가상환경 관련 캐시는 Library/Caches/pypoetry/virtualenvs/ … 이 디렉토리에 저장된다.

deactivate

 

가상환경을 끄고 싶다면 이렇게 입력하자. 다시 켜고 싶으면 poetry shell을 다시 입력하면 된다.

 

만약 중간에 실행을 하다가 파이썬 인터프리터가 poetry 가상환경을 인식 못 해서 패키지 의존성 관련 에러가 난다면, 파이썬 인터프리터에 poetry 가상환경 디렉토리가 제대로 설정되지 않았을 수 있다. 즉 현재 실행중인 환경의 디렉토리가 poetry 가상환경 정보가 들어있는 디렉토리랑 다를 수 있다.

 

맥의 경우, File>Manage IDE Settings>Settings Sync.. 로 설정으로 들어간 뒤 왼쪽 탭에서 Project를 누르고, python interpreter를 누른다. 여기서 Add Interpreter를 누르고 왼쪽 탭에서 poetry environment를 누른다. 여기서 Existing environment를 누른 뒤 경로를 다시 설정해야 한다.

 

경로는 poetry shell을 입력했을 때 “Spawning shell within …. ”하면서 경로가 나오는데, 그 경로에서 뒤에다 /bin/python 을 붙여준 경로를 입력하면 실행이 잘 될 것이다.

 

poetry 기타 커맨드

poetry 삭제하기

 

poetry를 삭제할 때는 poetry를 어떻게 설치했는지에 따라 삭제 방법이 다르다. 공식 사이트의 명령어를 통해 설치하지 않은 경우 버전 업데이트 등 다른 쪽에서 에러가 날 수 있는데, 이 경우는 기존에 poetry를 설치했던 방법으로 삭제해줘야 정상적으로 삭제된다.

 

-pip을 통해 설치한 경우

pip uninstall poetry

 

-brew를 통해 설치한 경우

brew uninstall poetry

 

그리고 poetry가 제거되었는지 확인하려면 반드시 poetry —version을 입력해서 poetry가 없다는 말이 나와야 한다. 그렇지 않으면 poetry 파일이나 메타 데이터가 컴퓨터 어딘가에 남아 있는 것이다.

 

만약 poetry —version 커맨드에서 버전이 나온다면, 맥의 경우는 최상위 폴더로 이동한 뒤 /.local/bin/poetry 파일이 있는지 확인해보자. 있다면 지우고 다시 설치하자.

 

공식 사이트의 poetry 설치 커맨드를 통해 설치한 경우에는 위와 관련된 문제는 아마 없지 않을까 싶다. 이 경우에는 이렇게 제거할 수 있다.

curl -sSL <https://install.python-poetry.org> | python3 - --uninstall

 

poetry python 버전 바꾸기

poetry env use {version}

 

poetry 버전 바꾸기

poetry self update {version}

 

poetry 환경 리스트 보기

poetry env list

 

참고한 포스트

https://python-poetry.org/docs/

https://python-poetry.org/docs/managing-dependencies/

'server-side > Django' 카테고리의 다른 글

django apps  (0) 2023.12.20
models and databases  (0) 2023.09.16
signals  (0) 2023.06.21
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11

(이 포스트는 우테코 테코톡 영상 - Spring Security 편의 내용을 정리하기 위해서 작성했습니다.)

 

Spring Security는 내부적으로 여러 클래스와 인터페이스들을 거침으로써 웹에서 인증(Authentication)과 인가(Authorization) 작업을 쉽게 구현할 수 있도록 한다. 

 

Spring Security 구조

내부 구조는 크게 보면 다음과 같다. 

1. AuthenticationFilter

2. AuthenticationManager

3. AuthenticationProvider

4. UserDetailsService

5. UserDetails

6. SecurityContextHolder

 

(1) AuthenticationFilter

Spring Security의 구조를 크게 살펴보면, 들어온 요청은 제일 먼저 AuthenticationFilter를 거친다. AuthenticationFilter에서는 앞으로의 인증 과정에서 사용하기 위해 UsernamePasswordAuthenticationToken(일종의 토큰으로 보면 된다)을 만들고, 이를 다음 단계인 AuthenticationManager 인터페이스에 전달한다. 

나중에는 AuthenticationManager로부터 인증이 완료된 User 객체를 받아서 SecurityContextHolder에 전달하는 역할까지 맡는다. 

(SecurityContextHolder는 스프링 시큐리티의 In-Memory DB라고 보면 된다.)

 

(2) AuthenticationManager

AuthenticationManager는 사실 인터페이스이고, 이를 구현한 클래스로는 ProviderManager가 있다. 이 인터페이스/구현 클래스에서는 AuthenticationFilter에서 넘어온 토큰이 유효한지 확인한 뒤(validate token credentials) 다음 단계인 AuthenticationProvider로 넘겨준다. 

 

(3) AuthenticationProvider

인증 작업이 이뤄지는 단계다. 토큰의 정보가 인증 서버에 있는 credentials 정보와 일치하는지를 검사한다. 일치하지 않으면 AuthenticationException을 발생시키고, 일치한다면 Authentication 토큰 객체에 추가 정보를 담는다. 

 

(4) UserDetailsService

이름처럼 UserDetails의 서비스 클래스라고 이해했다. 인증이 완료된 토큰을 UserDetails 인터페이스에게 넘겨준다. 

 

(5) UserDetails

인터페이스로, User 클래스가 이를 구현한다. UserDetailsService에서 받은 토큰의 정보를 사용해서 인증 서버나 일반 DB에서 유저 정보를 조회한다. 그렇게 조회된 User 객체를 다시 UserDetailsService에 전달한다.

이 User 객체는 (4), (3), (2), (1)을 거쳐서 (6)의 SecurityContextHolder로 전달된다. 

 

(6) SecurityContextHolder

스프링 시큐리티에서 사용하는 인메모리 저장소로 볼 수 있는데, SecurityContextHolder 안 SecurityContext 안 Authentication 부분에 인증 관련 정보들이 저장된다. 

그리고 유저 상태를 authenticated로 바꾼 뒤 요청을 DispatcherServlet에게 넘긴다. 그러면 DispatcherServlet에서 유저가 원래 요청했던 URL에 해당하는 컨트롤러의 메소드를 호출하여 응답을 리턴하게 된다. 

 

참고한 컨텐츠

https://www.youtube.com/watch?v=aEk-7RjBKwQ 

 

AWS RDS란?

Relational Database Service으로, 데이터베이스를 AWS 인스턴스로 올려서 원격으로 접속 가능하게 하는 서비스이다. 

보통은 AWS EC2나 S3을 사용해서 서버 인스턴스를 배포하거나 파일을 저장하는 용도로 사용하지만, RDS를 사용하면 DB를 원격으로 접근 가능하게 할 수 있다. 

 

이번에 RDS를 쓴 이유도 같은 DB 스키마를 가지고 협업할 때 공통으로 접근할 수 있는 DB가 있으면 좋을 것 같아서였다. 

각자 로컬에 DB를 만들고 마이그레이션해도 되지만, 그러다가 DB가 꼬일 수도 있기 때문이다. 

 

AWS RDS와 스프링 서버를 연결하는 방법은 총 두 단계이다. 

1. AWS RDS 인스턴스 시작

2. 스프링 서버 설정 변경

 

이중 1번 과정에서는 한 유튜브 영상을 참고해서 진행하였다. 

 

AWS RDS 인스턴스 시작

우선 AWS RDS 페이지에 접속하거나 aws rds를 검색한다. 

데이터베이스 생성 버튼을 누른다. 

 

데이터베이스 생성 방법에 정답이나 매뉴얼은 없지만, 내가 한 방식을 그대로 올려보겠다. 

(모든 항목 내용은 없고, 변경한 부분 위주로 올려보겠다.)

 

 

엔진 옵션-엔진 유형: MySQL.

어떤 데이터베이스를 사용할지 정한다. 

 

엔진 옵션-엔진 버전: MySQL 8.0.32

해당 버전이 DB 버전이 된다.

 

템플릿: 프리 티어

프리 티어를 제외한 프로덕션과 개발/테스트는 모두 사용 시 요금이 부과된다. 

 

설정-DB 인스턴스 식별자

DB 이름을 입력한다. 각 DB 인스턴스에 고유한 이름을 붙여준다고 생각하면 된다. 

 

설정-마스터 사용자 이름

DB에 접근하기 위해서 아이디(유저 이름)와 비밀번호를 요구하는데, 이중 아이디 영역이다. 이후 MySQL Workbench와 스프링 설정 변경에서 사용된다. 

 

설정-마스터 암호

DB에 접근하기 위해서 필요한 비밀번호이다. 이후 MySQL Workbench 설정과 스프링 설정 변경 시 사용된다. 

 

인스턴스 구성-DB 인스턴스 클래스

DB 인스턴스를 어떤 타입으로 할지를 정의하며, 나는 기본값인 db.t3.micro를 선택했다. 

 

앞서 요금이 나오지 않는 프리 티어를 사용했는데, 프리티어도 사용 제한이 있다. 

총 750시간 이상 사용하거나, 사용한 지 12개월이 지난 경우부터는 과금이 되므로 주의하자!

 

어쨌든 이 과정을 완료하면 RDS DB 인스턴스가 생성된다. 생성 과정은 몇 분이 걸린다. 

이 과정을 완료하고 DB가 실행되면 다음과 같은 화면이 나온다. 

 

 

DB 식별자를 눌러서 상세보기를 하면, DB 인스턴스에 접근할 수 있는 URL이 나온다. 이 URL과 아까의 유저 이름, 비밀번호를 통해서 DB에 접근할 수 있다. 

 

✅스프링 서버 설정 변경

앞서 설정한 엔드포인트, 포트 번호, 유저 이름, 비밀번호를 사용해서 설정을 변경하면 된다. 

application.properties 파일에서 다음 내용만 바꾸자. 

spring.datasource.url=jdbc:mysql://{$endpoint}:{$port}/{$schema_name}
spring.datasource.username={$username}
spring.datasource.password={$password}

 

$schema_name에는 해당 전체 DB의 database들 중 사용하고 싶은 database의 이름을 적어주면 된다. 

 

 

참고한 포스트

https://chung-develop.tistory.com/18

https://stackoverflow.com/questions/50837960/spring-boot-connection-to-mysql-remote-database

https://deeplify.dev/database/troubleshoot/unknown-initial-character-set-index

https://stackoverflow.com/questions/50855622/unknown-initial-character-set-index-255-received-from-server

https://zel0rd.tistory.com/141

https://velog.io/@dongzooo/My-sql-workbench-%EC%97%90%EB%9F%AC%ED%95%B4%EA%B2%B0-could-not-acquire-management-access-for-administration

(인프런 Spring Boot JWT Tutorial 강의Amigoscode 유튜브 강의를 참고하여 진행했습니다.)

 

인프런 강의 2강에서 SecurityConfig 클래스를 등록할 때 WebSecurityConfigurerAdapter 클래스를 상속받아 진행하는 부분이 있었는데, 현재는 WebSecurityConfigurerAdapter 클래스는 deprecated 된 상태라 사용할 수 없었다. 따라서 다른 유튜브 강의를 참고해서 진행하였다. 

 

Basic Authentication

프로젝트를 시작할 때 spring security를 dependency로 더하지 않았었다. MavenRepository에서 코드를 찾아 build.gradle 파일의 dependencies 변수에 추가해 준다. 

implementation("org.springframework.boot:spring-boot-starter-security")

 

그리고 기존에 작성한 /ok API를 실행하면 바뀌는 점이 있는데, 기존에는 바로 200 OK와 함께 문자열을 리턴하였던 API가 로그인 폼을 리턴한다. 

 

로그인 폼의 Username은 기본 상태에서는 "user"이고, 비밀번호는 서버를 실행시킬 때 일시적인 비밀번호가 부여된다. 

로그인한 이후에는 기존 /ok API와 같은 Response를 반환한다. 

똑같은 요청을 postman으로 보내보았다. 결과는 401 Unauthorized였다. 즉 /ok API로 보내진 요청이 unauthorized response를 받고 로그인 폼으로 리다이렉트(redirect) 되었던 것이다. 

 

왜 그랬는지는 Spring Security의 구조를 보면 알 수 있다.

Spring Security는 앱이 처음 시작되면 SecurityFilterChain 타입의 Bean을 찾는다고 한다. 이때 SecurityFilterChain은 인터페이스 타입이다. 

@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin(withDefaults());
    http.httpBasic(withDefaults());
    return http.build();
}

SecurityFilterChain은 다음과 같은 구조를 가진다. 

어떤 http 요청이 들어오건 모든 요청에 대해서는 해당 요청이 인가되었는지를 확인한다. 

만약 그렇지 않다면 로그인 폼을 렌더링하고, 여기서도 실패할 경우는 basic 폼을 렌더링한다. 

 

이제 이 코드를 그대로 가져와서 SecurityConfig 클래스를 정의해 보자. 

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.formLogin(withDefaults());
        http.httpBasic(withDefaults());
        return http.build();
    }

}

 

@EnableWebSecurity 어노테이션은 보안 관련 클래스와 Bean들을 포함하는 클래스에 명시한다. 그러면 스프링 컨테이너에서는 해당 클래스를 자동으로 Security Configuration 클래스로 인식한다. 

 

JWT Authentication

방금 전까지는 가장 기본적으로 Spring Security를 사용하는 경우를 살펴보았는데, 이번에 구현할 JWT 인증은 조금 더 복잡하다. 

우선 @Controller 클래스가 호출되기 전까지 요청에 관여하는 클래스들이 많다. 

 

Client -> JWTAuthFilter -> UserDetailsService -> (InMemory)DB -> SecurityContextHolderr -> Controller

 

해당 순서를 거쳐서 controller 클래스가 호출되고, 중간에 인증이 실패하면 아까처럼 401 Unauthorized 응답을 리턴하게 된다. 

 

(1) JWTAuthFilter

가장 먼저 요청을 받는 클래스로, 요청의 헤더에 'JWT'가 포함되어있는지를 확인한다. 만약 JWT가 없다면 JWT 토큰 관련 정보가 없는 것이므로 401 응답을 리턴한다. 

만약 JWT 헤더가 있다면 그 다음 단계인 UserDetailsService에다가 JWT 헤더의 토큰에서 추출한 유저 관련 정보를 가지고 해당 정보를 요청한다. (ex. id=1인 유저의 정보 요청)

UserDetailsService로부터 정보를 받으면 받은 유저 정보와 JWT 토큰을 디코딩해서 얻은 정보를 비교한다. 둘이 다르면 401 에러를 리턴하고, 같으면 SecurityContextHolder를 호출한다. 

 

(2) UserDetailsService

JWTAuthFilter의 요청을 받아서 그 다음 클래스인 DB(InMemoryDB일 수도, 유저 정보가 저장된 일반 DB일 수도 있고 구현은 다양하다)에다가 유저 정보를 요청하고, DB가 유저 정보를 리턴하면 그걸 JWTAuthFilter에게 전달한다. 

 

(3) DB

UserDetailsService의 요청을 받아서 DB에서 유저 정보를 조회한 뒤 UserDetailsService에게 리턴한다. 

 

(4) SecurityContextHolder

이 안에 SecurityFilterChain 인터페이스도 포함되어 있다. JWTAuthFilter에 의해 호출되면 이 클래스에서는 유저의 상태를 authenticated로 바꾸고 요청을 DispatcherServlet에게 넘긴다.

그러면 DispatcherServlet에서 유저가 원래 요청했던 URL에 해당하는 컨트롤러의 메소드를 호출하고 해당 컨트롤러에서 응답을 리턴하게 된다. 

 

JWT 인증 구현 - JwtAuthFilter

본격적인 JWT 인증을 구현하기 전, 하나의 dependency가 더 필요하다.

implementation("io.jsonwebtoken:jjwt-api:0.11.5")

 

이후 config 디렉토리에 JwtAuthFilter 클래스를 추가하고, 해당 클래스에 @Component 어노테이션을 붙인다. 

또한 해당 클래스가 OncePerRequestFilter 클래스를 상속하도록 한다. 

 

OncePerRequestFilter는 Filter의 일종으로, 한 번의 요청당 한 번만 호출되는 filter 클래스라고 보면 된다. 그리고 해당 클래스를 상속하려면 반드시 doFilterInternal() 메소드를 구현해야 한다. 한 번의 요청이 호출될 때 해당 filter 클래스가 어떤 작업을 할지를 이 메소드에서 정의한다고 보면 된다. 

 

구현하면 이런 모양이 되겠다. (메소드의 구현은 유튜브 영상 45:34에 나와 있다.)

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

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

 

JwtAuthFilter 메소드를 보면 Username을 얻는 부분과 토큰 유효성 검증 부분은 구현되어 있지 않다. 

이 부분은 jwtutils 파일을 검색하면 나오는 여러 깃헙 리포에서 가져올 수 있는데, 이들 중 하나에서 코드를 가져와서 사용했다. 

 

JwtAuthFilter.java에서 아직 구현되지 않은 부분을 메소드를 호출하여 완성하자. 

@Component
@RequiredAllArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // code
        userEmail = jwtUtils.extractUsername(jwtToken);
        final boolean isTokenValid = jwtUtils.validateToken(jwtToken, userDetails);
        // code
    }
}

 

이제 스프링 컨테이너에게 이 필터를 사용하겠다고 선언해야 한다. 

앞서 맨 먼저 만들었던 SpringConfig.java에는 @EnableWebSecurity 어노테이션이 붙어 있어서 스프링 컨테이너는 이 클래스를 security configuration 클래스로 인식한다. 따라서 이 클래스에 선언하면 된다. 

(코드의 구현은 유튜브 영상의 53:42에서 볼 수 있다.)

 

참고로 해당 영상에서는 Lombok를 dependency에 추가해서 모든 생성자 관련 코드를 @RequiredArgsConstructor으로 대체하는데, Lombok을 추가하지 않았다면 다음 작업으로 대체하면 된다. 

 

(예시) Lombok을 dependencies에 추가했을 경우

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

}

 

(예시) Lombok을 dependencies에 추가하지 않았을 경우

@Configuration
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Autowired
    public SecurityConfig(JwtAuthFilter jwtAuthFilter){
        this.jwtAuthFilter = jwtAuthFilter;
    }
}

 

JWT 인증 구현 - UserDetailsService

UserDetailsService를 구현하는 방법은 두 가지가 있다. 

UserDetailsService 인터페이스를 구현한 개별 클래스를 만들 수도 있고, SecurityConfig 클래스에 @Bean을 붙여 메소드로 구현할 수도 있다. 

강의에서는 비교적 간단한 메소드 구현 방식을 사용하였으나, 여기서는 circular import 에러 문제로 UserDetailsService 인터페이스를 클래스로 구현하여 사용하였다.

 

UserDetailsServiceImpl.java

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Autowired
    public UserDetailsServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberRepository
                .findByName(username)
                .stream()
                .findAny()
                .orElseThrow(() -> new UsernameNotFoundException("해당 유저가 없습니다."));
    }

}

 

이후의 과정에 대해서는 다음 포스팅에서 이어가 보겠다. 

 

 

참고한 포스트

https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html

https://www.youtube.com/watch?v=6n4k8Van_HI&list=PL93mKxaRDidERCyMaobSLkvSPzYtIk0Ah&index=3 

https://fusiondeveloper.tistory.com/69

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt#curriculum

https://velog.io/@suhongkim98/Spring-Security-JWT%EB%A1%9C-%EC%9D%B8%EC%A6%9D-%EC%9D%B8%EA%B0%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

https://velog.io/@pjh612/Deprecated%EB%90%9C-WebSecurityConfigurerAdapter-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8C%80%EC%B2%98%ED%95%98%EC%A7%80

https://www.youtube.com/watch?v=b9O9NI-RJ3o 

https://github.com/MajidLamghari/Spring-boot-security-jwt/blob/master/src/main/java/io/javabrains/springsecurityjwt/util/jwtUtil.java 

 

controller-service-repository 디자인 패턴

디자인 패턴 중 controller-service-repository 패턴을 사용해서 클래스를 만들고, 저번에 연결했던 mysql에 API를 사용해서 데이터가 저장되도록 해 보았다. 

 

컨트롤러의 경우, @RestController 어노테이션(Annotation)을 사용해서 해당 클래스를 REST 컨트롤러로 등록해 준다. 

@RestController는 REST API 기능을 수행할 때 붙이고, @Controller는 REST API가 아닌, 일반 뷰를 렌더링하는 용도의 클래스에 등록한다. 

 

또한 디자인패턴 클래스들 중 컨트롤러가 가장 먼저 요청(request)을 받게 되는데, 해당 요청을 다음 클래스인 서비스(service)로 넘겨주기 위해서, private final 타입으로 service 멤버 변수를 정의한다. 

 

private final로 선언하는 이유는 한번 서비스 클래스와 컨트롤러 클래스가 맵핑된 뒤 그 맵핑이 바뀌지 않게 하기 위해서이다.

 

또한 @Autowired를 생성자에 붙여서 컨트롤러가 생성될 때 매개변수로 서비스를 받도록 한다. 서비스와 컨트롤러가 같이 생성되도록 하기 위해서이다. 

 

그런데 스프링에서는 객체의 생성을 스프링 컨테이너에서 관리하므로, 서비스와 컨트롤러를 할당할 때 new 생성자를 쓰면 안 된다. 그냥 매개변수로 받은 서비스를 멤버 변수에 연결하면 된다. 

 

컨트롤러 클래스 내부에는 각 메소드를 정의할 수 있는데, 메소드 하나당 하나의 API가 된다. 

각 메소드에는 @GetMapping, @PostMapping 등의 어노테이션을 붙여서 해당 API가 어떤 HTTP 방식으로 작동할지를 명시한다. 또한 @GetMapping의 경우 @RequestParam 어노테이션을 붙여서 쿼리 변수를 받을 수도 있다. 

 

MemberController.java

@RestController
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService){
        this.memberService = memberService;
    }

    @GetMapping("/member")
    public Member member(@RequestParam(value = "name", defaultValue = "user") String name){
		// API logic
    }
}

 

서비스 클래스도 마찬가지이다. 컨트롤러 클래스가 로직 가장 위단에서 요청을 처리하는 반면(통신 관련 로직 등), 서비스 클래스에는 소위 말하는 비즈니스 로직이 포함된다. 

 

@Service 어노테이션으로 해당 클래스를 서비스 클래스로 등록한다. 마찬가지로 private final 멤버 변수로 리포지토리(repository)를 등록한 뒤, 생성자에 @Autowired를 붙여서 서비스 클래스가 생성될 때 리포지토리 클래스가 같이 생성 및 매칭되도록 한다. 

 

MemberService.java

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    public Member join(Member member){
        // business logic
    }
}

 

리포지토리 클래스의 경우 로직이 상대적으로 간단하다. 정확하게는 리포지토리 클래스가 아니라 리포지토리 인터페이스가 된다. 

 

리포지토리 인터페이스를 만들고, 해당 인터페이스가 <도메인 클래스 타입, 도메인 클래스 id 타입>JpaRepository를 상속받도록 한다. 

 

해당 JpaRepository 안에는 DB에 접근할 때 사람들이 많이 사용하는 메소드들(findById 등)이 들어 있기 때문에, 이 인터페이스를 상속하면 해당 메소드를 별도로 정의하지 않고 사용할 수 있다. 

 

다만 JpaRepository에서는 ID같은 PK 필드에 대해서만 findById 메소드를 제공하므로, 다른 컬럼을 기준으로 찾고 싶다면 해당 인터페이스에 추가로 메소드를 정의해주면 된다. 

 

MemberRepository.java

@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {
    Optional<Member> findByName(String name);

}

 

마지막으로 데이터베이스에 저장할 객체가 될 도메인(domain) 클래스를 정의한다. 

 

PK인 필드에는 @Id를 붙이고, DB의 AUTO_INCREMENT 옵션을 적용하고 싶다면 @GeneratedValue(strategy=GenerationType.AUTO)를 적용한다. 

 

Member.java

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    private String name;

}

 

이후 실행하면 다음과 같이 생성된 객체에 대한 json이 잘 나오는 것을 볼 수 있다. 

 

또한 mysql에도 데이터가 잘 저장된다. 

 

참고한 포스트

https://spring.io/guides/gs/rest-service/

https://velog.io/@leesomyoung/SpringBoot-SQLException-No-database-selected

 

이전에 들었던 스프링 강의에서는 Spring Data JPA를 사용하기 위해서 H2 데이터베이스를 사용했는데, 이게 용량이 작을뿐더러 실제 생활에서 많이 사용되는 데이터베이스는 아니라고 하셨다. 

그래서 Spring Data JPA를 Mysql과 연동하여 사용해 보려고 한다. 

 

우선 스프링에서 제공하는 '스프링과 mysql 연동하기' 가이드 코드를 참고했다. 

mysql 프로그램은 이전에 xampp를 통해 apache 서버와 함께 다운받아 뒀었어서 추가로 설치할 것은 없었다. 

 

Driver 클래스와 Driver 연동 에러

그런데 application.properties 파일 코드에서 에러가 났다.

 

해당 코드에서 에러가 발생한 것이었다. (지금은 에러를 해결한 상태라 파란색이지만, 원래는 붉은 글씨로 ClassException 에러가 났었다.) 찾아보니 application.properties에 명시한 driver 클래스가 실제 driver랑 연동이 되지 않아 class를 불러올 수 없던 것이었다.

 

그런데 나는 이미 mysql과 java의 driver인 connector 파일을 maven에서 import해서 gradle build 파일에 명시해 둔 상태였는데도 에러가 났다. 

 

select version(); 이라는 SQL문으로 mysql 버전을 확인해 보니, 내가 쓰는 mysql의 버전은 10.0 이상이었는데 maven에서 import한 코드는 mysql 8.0 이상의 버전에 대해서는 적용할 수 없었다. (java:5.1.6 버전의 connector는 mysql 8.0 이상을 지원하지 않는 것으로 보이는데, mysql 버전이 10.4.28이었다.)

 

그러므로 따로 mysql 홈페이지에 가서 mysql connector 8.0을 새로 다운로드 받았다. (connector 버전은 8.0이지만 mysql의 가장 최신 버전까지 java와 연동할 수 있다.)

 

다운받을 connector를 선택할 때는 드롭다운 메뉴에서 platform independent를 선택해 주고, tar과 zip 중에서는 아무거나 선택하면 된다. 우리가 필요한 것은 해당 파일에 있는 .jar 파일이다. 다운받은 파일에서 jar 파일만 따로 뺀 뒤, jar 파일의 디렉토리를 복사해 두자. 

 

Intellij에서 jar 파일 새로 적용하기

그리고 Intellij를 열어서 File -> Project Structure로 들어간 뒤 왼쪽 탭에서 Module을 클릭하고, 가운데 탭 중에서 Dependencies를 클릭한다. 그리고 작은 + 버튼을 누르고 '1 JARS or directories'를 누른 뒤 jar 파일을 찾아서 등록하면 된다. 

 

그리고 다시 application.properties 파일을 확인하면 클래스를 찾지 못해서 붉게 나타나던 코드가 파란색으로 정상적으로 보이게 된다. 

 

Mysql Dialect 설정 에러

 

그런데 여기서 또 다른 에러가 발생했다. 

JDBC 메타데이터 없이는 Dialect을 결정할 수 없다-는 에러였고, 해결하려면 javax.persistence.jdbc.url / hibernate.connection.url / hibernate.dialect 값들 중 하나를 설정해야 하는 것으로 보였다. 

 

위 세 값 중 javax.persistence.jdbc.url과 hibernate.dialect 값을 설정해서 오류를 해결할 수 있었다. 

javax.persistence.jdbc.url 값은 jdbc:mysql://localhost:포트번호를 적어주면 된다. 

hibernate.dialect 값은 org.hibernate.dialect.MySQLDialect로 입력하면 된다. 다른 DB를 사용한다면 다른 DB 버전으로 입력해주면 되겠다. 

 

위와 같이 두 에러를 해결했더니 잘 실행되는 모습을 볼 수 있다. 

 

Dialect란?

SQL 언어를 사용하는 많은 데이터베이스가 있다. 각 DB가 언어를 사용할 때는 SQL 언어를 그대로 사용하는 게 아니라 SQL에는 없는 부분을 따로 정의하기도 하고, SQL의 특정 부분은 구현하지 않거나, 조금씩 다른 형식의 문법을 사용한다. 이처럼 각 DB마다 사용하는 문법을 SQL Dialect라고 한다. SQL을 표준어, 각 DB에서 사용하는 방식을 방언에 비유한 것 같다. 

 

 

참고한 포스트

https://www.youtube.com/watch?v=_7R46uVZTyc 

https://velog.io/@k_ms1998/JPA-MySQL-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0

https://dev-coco.tistory.com/85 

https://spring.io/guides/gs/accessing-data-mysql/#initial

https://learnsql.com/blog/what-sql-dialect-to-learn/#:~:text=SQL%20Is%20the%20Language%20for%20Talking%20to%20Databases&text=PostgreSQL%2C%20MySQL%2C%20Oracle%2C%20and,call%20these%20variants%20SQL%20dialects.

 

Definition

Signal은 전체 프레임워크 내에서 어떤 이벤트가 발생했을 때, 그 이벤트의 발생을 알려주는 notification의 역할을 한다.

 

모든 signal은 django.dispatch.Signal 클래스의 인스턴스들이며, 사용자는 signal을 받을 수도 있고, 직접 signal을 만들 수도 있고, 많이 사용되는 signal 객체들을 받을 수도 있다. (주로 django.core.signals, django.db.models.signals 등 ~signals. 파일에 명시되어 있다.)

 

signal을 받거나 준다는 것은 무엇일까? signal을 받는다는 것은 어떤 이벤트가 발생했을 때 알림(notification)을 받는다는 것이고, signal을 준다는 것은 어떤 이벤트가 발생했을 때 알림을 보낸다는 것이다. signal을 받을 때는 해당 알림을 받아서 어떤 일을 할지를 receiver function으로 정의한다. 

 

반면 signal을 줄 때는 위에서 언급한 것처럼 django.dispatch.Signal 클래스의 인스턴스를 만든 뒤 Signal의 send() 함수를 이용해서 signal을 보낸다. 

 

Example

예시를 통해 살펴보자. 한 피자집에서 주문한 피자가 만들어졌을 때 소비자한테 알림을 보내려고 한다. 이때 피자집에서는 Signal을 만들고, 손님은 해당 signal을 받게 된다. 

 

우선 피자집의 입장에서 보면, 피자가 만들어졌을 때 signal 인스턴스를 생성한 뒤 send 함수를 이용해서 보내면 된다. 이때 누구에게 보내는지는 중요하지 않다. Signal을 보내는 쪽은 발신자를 특정하지 않고 보내고, 받는 쪽에서 자신이 원하는 signal만 골라서 받게 된다. 

# pizzastore.py
import django.dispatch

pizza_done = django.dispatch.Signal()
pizza_soldout = django.dispatch.Signal()


class PizzaStore:

    def send_pizza(self, toppings, size):
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)

    def notify_soldout(self, toppings):
        pizza_soldout.send(sender=self.__class__, toppings=toppings)

 

손님의 입장에서는 주문한 피자가 완성되었다는 signal을 받아야 한다.

 

이때, 위에서는 간단한 예시로 PizzaStore 클래스만 정의하였지만 실제로는 여러 가게들이 메뉴가 완성되었을 때 signal을 보낼 것이다. 손님이 원하는 건 자신이 주문한 피자 가게에서 보낸 signal이므로 해당 피자 가게가 보낸 signal만 받도록 설정할 수 있다. 즉 특정 발신자(sender)가 보낸 signal만 받도록 설정할 수 있다. 

 

아래 코드에서는 PizzaStore에서 보낸 pizza_done signal에 대해서만 notify_pizza 함수를 signal과 연결시킨다. 

# customer.py
from pizzastore import PizzaStore, pizza_done
from django.dispatch import receiver

@receiver(pizza_done, sender=PizzaStore)
def notify_pizza(sender, **kwargs):
    print("Pizza is done!")
    # code

 

위에서 언급한 @(decorator)를 사용하는 방법 말고도 signal과 receiver 함수를 연결할 수 있다. 

# customer.py
from pizzastore import PizzaStore, pizza_done
from django.dispatch import receiver

def notify_pizza(sender, **kwargs):
    print("Pizza is done!")
    # code
    
pizza_done.connect(notify_pizza)

 

즉 signal과 receiver 함수를 연결하는 방법은 두 가지가 있다.

1. @(decorator)를 사용한 연결

2. connect 함수를 사용한 연결

 

sender 옵션은 필수 옵션은 아니다. sender 옵션을 지정하지 않을 경우, notify_pizza는 PizzaStore가 아닌 다른 클래스에서 생성한(sender가 다른 클래스로 되어 있는) pizza_done signal이 발생할 때에도 실행될 것이다. 

 

Signal.send(). vs Signal.send_robust()

signal을 보낼 때는 .send() 함수 외에도 .send_robust() 함수를 사용할 수 있다. 두 함수 모두 (해당 signal을 받는 receiver 함수, 해당 함수가 리턴하는 값)의 튜플 리스트를 리턴한다. 

 

다만 .send() 함수는 개별 receiver 함수로 signal을 보낼 때 에러가 발생할 경우 그 에러를 처리하지 않는다. 그래서 일부 receiver 함수에서 오류가 발생할 경우, 그 함수는 signal을 받지 못할 수 있다. 

 

반면 .send_robust() 함수는 signal을 보내는 도중 발생한 모든 에러를 처리한다(catch로 처리하는 것으로 보인다). 그래서 일부 receiver 함수에 signal을 보내다가 에러가 발생한 경우, .send_robust() 함수의 값으로 리턴되는 tuple 중 일부는 (에러가 발생한 함수, 발생한 에러의 종류)로 값이 리턴된다. 어떻게 해서든지 signal을 보낸다는 의미에서 send_robust라고 이름 붙여진 것 같다. 

 

Receiver 함수의 실행 횟수 제한

receiver 함수는 기본적으로 signal을 받을 때마다 실행되는데, 이 방식이 문제가 될 수도 있다. 

예를 들어 어떤 모델 인스턴스가 저장될 때마다 사용자에게 이메일이 가는 것보다, 한 번만 가는 것이 더 바람직하다. 

이때는 receiver 함수에 dispatch_uid라는 새로운 옵션을 선언하고 그 값으로는 해싱이 가능한 문자열이면 어떤 것이든 선언할 수 있다. 

pizza_done.connect(notify_pizza, dispatch_uid="evening_pizza")

 

참고한 포스트

https://docs.djangoproject.com/en/4.2/topics/signals/

 

'server-side > Django' 카테고리의 다른 글

models and databases  (0) 2023.09.16
python - poetry 사용하기  (0) 2023.07.12
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11
Model.objects.filter() vs Model.objects.get()  (0) 2022.07.11
models: on_delete  (0) 2022.07.05

+ Recent posts