Models


장고의 models.Model 클래스를 활용하면 직접 SQL을 사용하지 않고도 파이썬에서 객체 타입을 선언하고 DB와 연결시킬 수 있다. models.Model을 상속한 모델 클래스를 만들면 장고가 제공하는 ORM(object-relational mapping)을 이용할 수 있다.
models.Model을 상속한 클래스 내부에는 필드를 정의할 수 있고, 이 필드들도 models 라이브러리의 세부 속성으로 선언할 수 있다.
DB와 연결되었을 때 하나의 클래스는 하나의 테이블이 되고, 클래스 내부의 필드들은 각 테이블 내부의 컬럼들이 된다.

다음은 장고 모델 클래스의 예시이다.

from django.db import models

class User(models.Model):
    id = models.AutoField()
    name = models.CharField()
    age = models.IntegerField()


User 클래스는 장고와 연결된 DB에서 user 테이블이 되고, id, name, age 속성들은 user 테이블 내부의 id, name, age라는 이름의 컬럼들이 된다. 참고로 실제 테이블 이름은 user이 아닐 수 있다.
장고에서는 테이블 이름을 지을 때 기본으로 사용되는 규칙이 있어서 별도의 이름을 지정하지 않으면 그 규칙대로 이름이 지정되고, 원하는 이름이 있으면 커스텀 속성으로 지정할 수 있다.

보통은 'app이름_model이름' 으로 테이블 이름이 지정된다. 만약 User 클래스가 account라는 app 내부에서 정의되었다면, 기본적으로 해당 테이블은 account_user이 된다. 직접 테이블 이름을 지정할 수도 있다. 각 모델 클래스의 내부에는 Meta 클래스를 선언하여 테이블 자체와 관련된 정보를 설정할 수 있다.
db_table 속성에다가 지정하고 싶은 테이블 이름을 입력하면 된다. 아래와 같은 경우 DB에 저장되는 테이블 이름도 User가 된다.

from django.db import models

class User(models.Model):
    id = models.AutoField()
    name = models.CharField()
    age = models.IntegerField()
    
    class Meta:
        db_table = "User"


장고에서 모델 클래스(models.Model을 상속한 클래스)를 정의하면 기본적으로는 DB에 테이블이 생기고, 변경되는 점들이 있으면 마이그레이션(migration)을 통해 연결되는 것이 대부분이다. 그러나 Meta 클래스에서 다른 옵션을 선택하면 이 규칙들을 바꿀 수 있다.


1) abstract = True

장고에는 추상 모델(abstract model)이 있다. 이렇게 선언하면 장고 내부에서는 해당 클래스를 모델로 인식하지만, 연동된 데이터베이스에 테이블이 생기지는 않는다.
보통은 추상 모델 클래스를 만들어 놓고, 다른 모델에서 해당 클래스를 상속하는 방식으로 사용한다. 한 모델 클래스를 상속받는 다른 모델 클래스는 그 모델 클래스에 정의된 필드를 사용할 수 있다.


2) managed = False

장고에서 모델 클래스에 변경사항이 있으면 마이그레이션 커맨드(makemigrations, migrate 등)를 통해 반영할 수 있는 이유는 테이블에서는 이 옵션이 기본으로 managed=True로 되어 있기 때문이다. 그러나 이 옵션을 False로 저장하면 해당 모델을 추가하거나 내부 필드를 변경해도 장고와 연결된 DB에 변경사항이 반영되지 않는다. 변경사항을 반영하려면 직접 해당 DB에 SQL문을 입력해야 한다.
이 옵션은 주로 해당 모델이 장고 프로젝트 내에서만 사용되는 모델이 아닐 때 등에 사용된다.


3) proxy = True

이 속성을 지정한 모델은 기존에 있던 다른 모델의 프록시 모델이 된다. 즉 다른 모델과 똑같이 작용한다고 보면 된다.

from django.db import models

class User(models.Model):
    # contents
    
class Person(User):
    # contents
    
    class Meta:
        proxy = True


모델 Person이 모델 User의 프록시 모델이라고 해 보자. User.objects.all()과 Person.objects.all()은 모두 같은 결과를 리턴한다.
프록시 모델을 선언하는 이유 중 하나는 DB와 장고 모델과의 연결은 그대로 두고, User 모델이 장고 내부에서 작동하는 방식이나 메소드를 바꾸고 싶을 때이다. 기본 모델인 User와 달리 Person에서는 정렬(ordering) 방식을 다르게 한다던가, 내부 메소드를 추가로 정의하고 싶을 때 프록시 모델을 사용한다.

 

Managers

 

쿼리 등으로 장고의 모델과 데이터베이스 사이에 통신이 일어날 때, 모델과 데이터베이스가 직접적으로 통신하지 않는다. 그 사이에는 Manager 클래스가 있다.
이 manager 클래스는 한 모델마다 최소 1개 이상이 있다. 즉 1:M의 관계다. 기본적으로는 1개이지만 원한다면 하나의 모델에 여러 개의 매니저 클래스를 둘 수 있다.
우리가 알고 있는 기본 매니저 클래스가 바로 objects이다. 항상 쿼리를 날릴 때 Model.objects.all() 이라고 썼었는데, 그 objects가 바로 매니저 클래스였던 것이다.

매니저 클래스는 쿼리셋을 만드는 방식이나 쿼리셋의 결과를 다르게 하고 싶을 때에도 사용한다.
예를 들면 Person 모델에는 employee_type이 crew, chef, administrator인 세 종류의 직원이 있다고 해 보자. 만약 employee_type이 crew인 직원들 안에서만 쿼리를 수행하고 싶다면 매번 filter() 메소드를 거는 방법도 있지만, 아예 crew_members 라는 매니저 클래스를 생성하고 필터링된 쿼리를 적용한다면 더 편하고 직관적으로 쿼리를 작성할 수도 있다.

# models.py
from django.db import models

class Person(models.Model):
    # contents
    objects = models.Manager()
    crew_objects = PersonCrewManager()
    administrator_objects = PersonAdministratorManager()
    chef_objects = PersonChefManager()
    
class PersonCrewManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="crew")
    
class PersonAdministratorManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="administrator")
        
class PersonChefManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(employee_type="chef")
# example.py
from models import Person

Person.objects.all()
Person.crew_objects.all()
Person.administrator_objects.all()
Person.chef_objects.all()

 

 

QuerySet

 

장고 모델에서 DB에 연결하여 정보를 가져오기 위해서 사용하는 것이 쿼리셋이다. 장고 문법으로 쿼리를 작성하면 해당 쿼리셋은 DB의 SQL로 변환되어 DB에 전달되고, DB에서 나온 데이터는 다시 장고에서 쿼리셋으로 변환된다. 

 

예를 들어서 다음과 같은 쿼리를 작성하면, DB에는 이에 해당하는 SQL문이 입력된다. 

Person.objects.get(id=1)
SELECT * FROM myapp_person WHERE id = 1;

 

사실 쿼리셋은 코드가 실행되자마자 SQL에서 데이터를 가져오지는 않는다. 즉 쿼리셋은 작성되는 시점과 데이터를 가져오는 시점(evaluate 되는 시점)이 다르다. 그래서 'querysets are lazy'라는 말이 있다. 

변수에 쿼리셋을 할당할 때는 DB를 거치지 않고, 결과물을 콘솔 등으로 print()해서 보여줄 때 DB를 거쳐서 결과를 가져오게 된다. 

 

또한 쿼리는 캐싱을 사용한다. 한 번 evaluate 된 결과를 다음에 사용할 때는 또 데이터베이스를 거치지 않고(no hit), 캐싱해둔 데이터를 사용한다는 것이다. 그런데 그 결과값을 변수에 할당해 두지 않으면 같은 쿼리 결과를 사용할 때도 데이터베이스를 계속 hit 하게 된다. 

 

또한 한 모델 테이블과 연결된 여러 모델의 데이터도 select_related()prefetch_related()를 통해서 한 번에 불러올 수 있다. 이는 특히 for loop 등으로 반복적인 작업을 계속할 때 효율적이다. 

해당 모델이 참조하는 모델 데이터를 불러올 때는 select_related(), 해당 모델을 참조하는 모델 데이터를 불러올 때는 prefetch_related()를 불러온다. many-to-many 관계의 모델 데이터도 prefetch_related()로 불러올 수 있다. 

 

캐싱이 사용되는 경우 -> 효율적

people = Person.objects.select_related("company")		# Person, Company 데이터를 모두 가져옴
for person in people:
    person.company.is_valid = True		# 여기서는 database hit 발생 X

캐싱이 사용되지 않는 경우 -> 비효율적

people = Person.objects.all()		# 여기서 Person 모델 데이터만 가져옴
for person in people:
    person.company.is_valid = True		# 한 번 실행될 때마다 database hit

 

 

복잡한 쿼리셋

 

쿼리셋에서 filter(), exclude() 등으로 여러 개의 조건을 걸 수 있는데, 간혹 복잡하거나 구체적인 조건도 설정할 수 있다. 

 

1) field lookups

말 그대로 필드의 값과 동일한 값만 찾는 게 아니라, 숫자일 경우 gt, gte, lt, lte 등으로 작거나 큰 값을 조회할 수 있다. 문자열인 경우는 contains, iexact 등으로 특정 문자열을 포함하거나 대소문자를 구분하지 않은 결과들도 조회할 수 있다. 이런 field lookup등의 경우 필드 이름 뒤에 '__'을 붙인다. 

더 복잡한 field lookup도 있다. '__'을 붙이면 해당 모델과 관계 있는 다른 모델에서의 검색도 가능하다. 이 FK 검색은 연쇄적으로 계속 일어날 수 있는데, 실제로 이렇게 쿼리를 작성할 경우 SQL문에서는 JOIN 문을 사용해서 결과를 가져온다고 한다. 

# Person의 employee_type 필드값에 'c'를 포함한 모든 레코드 조회
Person.objects.filter(employee_type__contains="c")

# Person의 foreignkey인 Company 모델의 name 필드값에 'a'를 포함한 모든 레코드 조회
Person.objects.filter(company__name__contains="a")

 

2) Q()

Q() 안에는 filter(), exclude()에 사용하는 것처럼 조건문이 온다. 다만 Q()를 여러 개 사용해서 AND, OR, XOR 등 다양한 조건이 교차하는 필터링을 할 수 있다. 기존의 chaining 방법으로는 모든 조건들을 전부 만족하는 쿼리만 불러올 수 있지만 Q()를 사용하면 조건들을 다양하게 선언할 수 있다. 

# Person 중 name에 'peter'이 포함되고 27세 이상인 레코드 조회
Person.objects.filter(Q(name__contains="peter") | Q(age__gte=27))

 

3) F()

F()를 사용하면 기존의 다른 쿼리와는 달리 여러 필드들의 값을 비교할 수 있다. 기존에는 한 필드의 값에만 조건을 걸 수 있었다. 하지만 F()를 사용하면, 예를 들면 두 필드값을 비교하는 등의 필터링도 가능하다. 또한 필드값을 가져와서 연산 후에 조건을 거는 것도 가능하다. 

# House 레코드 중 number_of_people 필드와 number_of_cars 필드값이 같은 레코드만 조회
House.objects.filter(F("number_of_people") == F("number_of_cars"))

# House 레코드 중 price가 annual_people_income 필드값의 2배 이상인 레코드만 조회
House.objects.filter(price__gt=2*F("annual_people_income"))

 

4) Aggregation

또한 aggregate()를 사용하면 레코드의 총 개수, 특정 필드의 총합 등 특정 값을 추가해서 계산할 수 있다. Count, Avg, Max, Sum 등 다양한 기본 함수들을 사용할 수 있다. 

# 전체 Person 레코드의 개수 조회
Person.objects.aggregate(Count("id"))

 

5) Annotate

annotate()를 사용하면 쿼리셋에 포함되는 각 인스턴스 별로 aggregate()와 같은 쿼리를 생성할 수 있다. 

# 피자에 들어가는 토핑의 개수를 같이 조회
pizzas = Pizza.objects.annotate(number_of_toppings=Count("toppings")

# 첫 번째 피자에 들어간 토핑의 개수
pizzas[0].number_of_toppings

 

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

django routers  (1) 2023.12.21
django apps  (0) 2023.12.20
python - poetry 사용하기  (0) 2023.07.12
signals  (0) 2023.06.21
Model.select_related() vs Model.prefetch_related()  (0) 2022.07.11

이 포스트는 인프런의 스프링부트 시큐리티 강의를 참고하여 작성되었습니다. 

 

 

프로젝트 구조

구글 로그인은 소셜로그인의 일종이기 때문에 스프링 시큐리티를 사용할 때 OAuth와 함께 사용해야 한다. 홈페이지에서는 일반 로그인과 구글로그인을 같이 구현하는 것이 목표이므로, 일반 로그인의 코드를 따로 건드리지 않고 구글 로그인 코드를 작성해 주어야 하겠다. 

 

일반 로그인 관련 코드는 auth라는 패키지에, 소셜 로그인 관련 코드는 oauth 패키지에 따로 묶어두자. 

 

스프링 시큐리티를 사용할 때 가장 기본이 되는 클래스는 SecurityConfig 클래스다. 이 클래스 내부에는 SecurityFilterChain 인스턴스를 리턴하는 filterChain() 메소드가 재정의된다. filterChain() 메소드는 스프링 시큐리티가 개입할 때 가장 먼저 호출되는 메소드이다.

 

이 클래스에 @EnableWebSecurity 어노테이션을 붙여서 내부에 정의된 filterChain() 메소드를 스프링 필터체인(filterChain)에 등록한다. 

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

    @Autowired
    public PrincipalOauth2UserService principalOauth2UserService;

    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.csrf(csrf -> csrf.disable());
        http
                .cors(cors -> cors.disable())
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/login", "/loginForm", "/loginProc", "/join", "/joinProc", "/user").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin((formLogin) ->
                        formLogin
                                .loginPage("/loginForm")
                                .loginProcessingUrl("/loginProc")
                                .defaultSuccessUrl("/"))
                .oauth2Login((oauth2Login) ->
                        oauth2Login
                                .loginPage("/loginForm")
                                .userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig.userService(principalOauth2UserService))
                )
                .httpBasic(withDefaults());
        return http.build();
    }

}

 

HttpSecurity.cors(), HttpSecurity.csrf()의 내부에서는 CORS, CSRF 처리를 어떻게 할지를 정의할 수 있다. 

 

HttpSecurity.authorizeHttpRequests() 내부에서는 프로젝트의 각 엔드포인트에 대해 접근권한을 허용할지 말지를 결정할 수 있다. requestMatchers() 안에 url 패턴을 선언해서 해당 패턴들을 가진 엔드포인트들을 한 번에 처리할 수도 있다. 

 

뒤에 authenticated()를 붙이면 해당 엔드포인트들에 대해서는 인증을 실행하고, permitAll()을 붙이면 해당 엔드포인트에 대한 요청들은 별도의 인증을 안 하고 통과시킨다. 

 

HttpSecurity.formLogin() 내부에서는 시큐리티 기본 로그인을 어떻게 할지를 설정한다. 기본 로그인이란 아이디, 비밀번호를 입력해서 사용자를 인증하는 로그인이다. 메소드 내부에서는 .loginPage()로 어떤 엔드포인트를 기본 로그인 페이지로 설정할지를 정할 수 있다. 이 경우 인증되지 않은 사용자가 인증이 필요한 페이지를 요청할 경우 기본 로그인 페이지로 리다이렉트 된다. 

 

.loginProcessingUrl()은 로그인 과정을 처리하는 엔드포인트다. 보통 로그인 폼에서 '로그인'을 눌렀을 때 로그인 과정을 처리하는 엔드포인트로 이동하고, 해당 엔드포인트에서 로그인 인증이 진행된다. 

 

.defaultSuccessUrl()은 로그인이 성공했을 때 리다이렉트 될 엔드포인트이다. 

 

HttpSecurity.oauth2Login() 내부에서는 OAuth 로그인을 어떻게 설정할지를 작성한다. .loginPage()에서 설정한 엔드포인트로부터 들어오는 요청들에 대해서 oauth provider(구글, 네이버 등등)가 인증을 진행한다. 로그인 페이지 말고도 여러 엔드포인트를 설정할 수 있다. 

 

그 전에, OAuth에서는 3가지의 주체가 있다: Client, Resource Owner, Resource Server

client는 소셜로그인을 이용하려는 웹사이트의 서버, resource owner는 사용자, resource server는 구글 등 소셜로그인 서비스를 제공하는 서버이다. 

 

 

스프링 시큐리티 OAuth 2.0에서 지정한 엔드포인트(protocol endpoint)

1. Authorization Endpoint

client가 resource owner로부터 권한(authorization)을 획득하는 부분이다. 

즉 사용자(resource owner)의 로그인이 진행되는 부분이다. 사용자가 로그인을 진행하면 그 과정에서 앱 서비스(client)는 권한 코드(authorization code)를 얻을 수 있다. 사용자의 로그인이 성공적이었다면 1번에서 권한 코드를 얻을 수 있다. 

 

2. Token Endpoint

1번에서 얻은 권한 코드를 액세스 토큰으로 교환하는 지점이다. 

 

3. Redirection Endpoint

인증 서버(resource server)가 앱 서비스(클라이언트)에게 권한 정보(authorization)를 리턴하는 지점이다. 

 

4. UserInfo Endpoint

클라이언트가 인증된 사용자의 정보(claims)를 받기 위해서는 이 엔드포인트로 요청하면 된다. 

 

코드에서는 .userInfoEndpoint()로 사용자의 인증 정보를 받아서 어떻게 처리할지를 설정했다. 

.userInfoEndpoint().userService() 에서는 사용자의 인증 정보를 엔드포인트로 받아서 어떤 UserDetailsService 클래스에 넘겨줄지를 설정한다. 여기서는 앞에서 미리 작성한 principalOauth2UserService 클래스에게 유저 정보를 넘겨주기로 설정했다. 

 

 

OAuth 서비스 클래스 작성 - 기존 로그인 로직과 연동

principalOauth2UserService 클래스는 DefaultOAuth2UserService 클래스를 상속하고, DefaultOAuth2UserService 클래스는 OAuth2Service 인터페이스를 구현한다. 

OAuth2Service 인터페이스는 UserInfo Endpoint에서 유저 정보를 얻은 뒤 그에 맞는 OAuth2User 인터페이스 타입의 인스턴스를 리턴한다. 즉 우리 클래스는 이 인터페이스를 구현받은 클래스를 상속받고 있으므로 OAuth2Service 인터페이스의 역할 + a를 한다고 볼 수 있다. 

 

해당 클래스는 앞서 구현 및 상속받은 인터페이스, 클래스가 있기 때문에 loadUser() 라는 메소드를 재정의해야 한다. loadUser() 메소드는 UserInfo Endpoint에서 유저 정보를 얻은 뒤 그에 맞는 OAuth2User 인터페이스 타입의 인스턴스를 리턴한다. 인터페이스 타입을 리턴할 수는 없으므로, OAuth2User 인터페이스를 구현한 클래스를 하나 만든 뒤 그 클래스 타입의 인스턴스를 리턴시켜야 하겠다. 

 

(예시 코드)

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        OAuth2User oauth2User = super.loadUser(userRequest);
        // code
        return new PrincipalDetails(userEntity, oauth2User.getAttributes());
    }
}

 

여기서 PrincipalDetails라는 클래스는 강의에서 정의한 클래스로, 기존 로그인에서 필요했던 UserDetails 인터페이스와, loadUser()의 리턴 타입인 OAuth2User 인터페이스를 구현한 클래스이다. 

 

앞서 기본 로그인을 진행하기 위해서는 PrincipalOauth2UserService와 같은 역할을 하는 서비스 클래스로 PrincipalDetailsService 클래스를 정의했었고, 해당 클래스는 UserDetailsService 라는 인터페이스를 구현하고 있었다. UserDetailsService 인터페이스에서는 loadUserByUsername() 메소드를 오버라이딩 해야 했는데, 이 메소드의 리턴 타입이 UserDetails 이었다.

 

즉 PrincipalDetailsService와 PrincipalOauth2UserService의 관계는 매우 비슷하다. 각각 UserDetailsService와 OAuth2Service 인터페이스를 구현하고, 그 인터페이스에 선언된 메소드에서는 각각 UserDetails와 OAuth2User 인터페이스 타입을 리턴한다. 

 

그러므로 PrincipalDetails 클래스에서 두 인터페이스(UserDetails, OAuth2User)를 모두 구현해버리면 하나의 클래스가 일반 로그인 서비스인 PrincipalDetailsService와 소셜로그인 서비스인 PrincipalOAuth2UserService 모두에서 쓰일 수 있게 된다. 

 

대신 일반 로그인과 소셜로그인의 처리 절차가 다를 수 있으므로, UserDetailsService의 loadUserByUsername() 메소드와 OAuth2UserService의 loadUser() 메소드를 각각 다르게 오버라이딩 해 주면 같은 엔드포인트와 같은 클래스 타입을 사용해서 두 가지의 로그인을 한번에 진행할 수 있다. 

 

구조 설명

 

해당 그림은 스프링 시큐리티 구조를 간단히 나타낸 것이다. 

 

앞서 언급한 PrincipalOauth2UserService와 PrincipalDetailsService는 그림에서 'UserDetailsService' 부분에 해당한다. 이 클래스들은 AuthenticationProvider에서 authentication 객체를 건네받은 뒤, 해당 객체를 통해 UserDetails 인터페이스에 접근해서 유저 정보를 가져온 뒤 다시 AuthenticationProvider에게 그 정보를 리턴하는 역할을 한다. 

 

또한 앞서 나왔던 PrincipalDetails는 UserDetails 부분에 해당한다. 보통 모델에서 사용하는 유저 클래스가 이 인터페이스를 구현한다. 이 인터페이스에는 스프링 시큐리티에서 해당 유저 모델을 사용하기 위해서 필요한 여러 필드와 메소드(개별 유저의 권한 리턴하기, 유저가 유효한지 확인하는 메소드 등)가 선언되어 있다. 

 

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

 

+ Recent posts