admin 파일에 코드를 작성한 뒤, 작성한 내용을 관리자 페이지에 반영하기 위해서 필요한 작업이다.
작성한 modelAdmin을 register으로 등록하는 방법은 두 가지가 있다.
1. 메소드 사용하기
admin.site.register(모델, 모델 어드민 클래스)
class AuthorAdmin(admin.ModelAdmin):
// code
admin.site.register(Author, AuthorAdmin)
2. 데코레이터(decorator) 사용하기
장고에서 @을 사용하여 추가적인 기능을 제공하는 것을 데코레이터(decorator)라고 한다.
@admin.register(모델)
class 모델 어드민 클래스(admin.ModelAdmin):
// 세부 코드
@admin.register(Author):
class AuthorAdmin(admin.ModelAdmin):
// code
또한 하나의 어드민 클래스에서 여러 모델을 관리할 수도 있기 때문에, 여러 개의 모델을 입력할 수도 있다.
@admin.register(모델1, 모델2, 모델3)
class모델 어드민 클래스(admin.ModelAdmin):
// 세부 코드
관리자 페이지가 다른 어플리케이션과 연결되는 원리
settings.py의 INSTALLED_APPS 필드에는 기본값으로 django.contrib.admin이 추가되어 있다.
이 클래스가 settings.py에 추가되어 있다면, 앱(어플리케이션, 서버)이 시작하자마자 django.contrib.admin 클래스의 모듈은 INSTALLED_APPS 안에 있는, admin을 사용하는 다른 앱에 import 된다.
이게 가능한 이유는 django.contrib.admin의 autodiscover() 함수 때문이다.
autodiscover() 메소드는서버가 시작하자마자 INSTALLED_APPS에 등록된 다른 앱에서 admin을 사용하는 모델을 찾고, 그 모델들을 관리자 사이트에 등록시킨다.
autodiscover() 메소드는 서버 시작 시 모든 admin에 등록할 모델들을 자동으로 불러오지만, 관리자 페이지를 커스터마이징했을 때 일부 상황에서는 이 기능이 필요하지 않을 수 있다.
autodiscover() 메소드를사용하고 싶지 않다면, django.contrib.admin 클래스 하위에 있는 apps.SimpleAdminConfig 클래스를 사용해야 한다. 원래 django.contrib.admin은 apps.AdminConfig 메소드를 기본값으로 사용했었다.
즉 django.contrib.admin은 아무 옵션도 추가하지 않을 경우 기본값으로 django.contrib.admin.apps.AdminConfig 클래스를 사용해 왔던 것이다.
그러나 autodiscover() 메소드를 사용하고 싶지 않다면 settings.py의 INSTALLED_APPS에 django.contrib.admin 을 지우고 django.contrib.admin.apps.SimpleAdminConfig를 추가하면 된다.
ModelAdmin 인터페이스의 옵션들
ModelAdmin은 규모가 큰 인터페이스이고, 그만큼 커스터마이징을 위한 여러 옵션을 추가할 수 있다.
옵션은 각 어드민클래스의 필드 형식으로 지정하면 된다.
1. actions
actions의 값으로는 함수들의 리스트를 입력한다.
해당 어드민 클래스에서 사용할 수 있는 함수, 기능들을 나열할 수 있다.
2. actions_on_top / actions_on_bottom
True나 False를 입력한다.
actions 옵션으로 입력한 기능들을 페이지의 위에 표시할지, 아래에 표시할지를 지정한다.
class ProjectAdmin(admin.ModelAdmin):
actions_on_top = True
3. date_hierarchy
모델의 필드 중 DateField 또는 DateTimeField 타입인 필드 이름을 값으로 갖는다.
해당 모델의 데이터를 정렬할 때 입력한 필드의 날짜 순서대로 정렬해 준다.
역순으로 정렬하고 싶다면 필드 이름 앞에 - 을 붙이면 된다.
class ProjectAdmin(admin.ModelAdmin):
date_hierarchy = '-register_date'
4. empty_value_display
문자열을 값으로 받는다.
두 가지 방법으로 사용할 수 있다.
첫째로, 각 어드민 클래스의 필드 이름으로 empty_value_display를 사용해서 지정할 수 있다.
해당 모델의 필드 중 빈 값(null 또는 "" 공백 문자열)이 있다면 그 값을 관리자 페이지에서 어떻게 표시할지를 지정한다.
class UserAdmin(admin.ModelAdmin):
empty_value_display = 'EMPTY'
둘째로, 데코레이터를 사용해서 각 필드값을 리턴하는 함수 위에 붙여서 사용할 수 있다.
첫번째 방법에서는 해당 모델에 속한 모든 필드의 빈 값을 같게 나타내지만, 두 번째 방법을 사용하면 각 필드별로 빈 값을 다르게 표시할 수 있다.
class UserAdmin(admin.ModelAdmin):
exclude = ['birth-date', 'age']
// birth-date, age 필드를 제외한 모든 필드가 관리자 페이지에 나타남
6. fields
필드 이름 튜플을 값으로 받는다.
데이터를 수정(change)이나 추가(add)하는 페이지에서 fields의 값에 포함된 필드들만 나타나게 할 수 있다.
serializer.py에서 read-only로 지정된 필드들만 fields 리스트에 포함될 수 있다.
더 세부적인 작업을 하고 싶으면 fieldsets 옵션을 사용하자.
# models.py
class Homework(models.Model):
title = models.CharField('제목')
content = models.TextField()
teacher = models.ChoiceField(choices=TEACHER_LIST)
submission_date = models.DateTimeField()
# admin.py
class HomeworkAdmin(admin.ModelAdmin):
fields = ('title', 'content')
위 예시의 경우, 데이터를 추가 및 수정하는 페이지에서는 title과 content 필드에 대해서만 수정 및 추가 작업을 할 수 있다.
또한, fields 튜플 안에서 괄호()를 한번 더 사용하면, fields 튜플 안의 필드들을 한 줄로 나타낼 수 있다.
7. fieldsets
fields와 기본적인 역할은 같고, 튜플을 원소로 갖는 튜플 리스트(two-tuple)를 값으로 받는다. 처음 관리자 페이지에서 보는 카테고리 화면이 아니라, 데이터를 추가나 수정하는 페이지에서 선택한 필드를 그룹으로 묶어서 나타내는 데 사용한다.
fields 옵션에서는 필드 그룹이 하나만 있었다면, fieldsets 옵션에서는 하나의 모델에 대해서도 여러 필드 그룹을 만들 수 있다.
여러 개의 그룹을 만들고 싶다면 fieldsets 안에서 튜플을 여러 개 만들면 된다. 각 튜플은 "옵션"과 {} 딕셔너리로 이루어진다.
이런 식으로 만들면 된다.
fieldsets = (
("그룹_1의 이름", {
"fields": ('그룹_1에 들어갈 필드_1', '그룹_1에 들어갈 필드_2')
}),
("그룹_2의 이름", {
"fields": ('그룹_2에 들어갈 필드_1', '그룹_2에 들어갈 필드_2'),
})
)
# models.py
class Course(models.Model):
student_name = models.CharField()
lecturer_1 = models.ChoiceField(choices=LECTURER_LIST)
course_1 = models.ChoiceField(choices=LECTURER_LIST)
lecturer_2 = models.ChoiceField(choices=LECTURER_LIST)
course_2 = models.ChoiceField(choices=LECTURER_LIST)
lecturer_3 = models.ChoiceField(choices=LECTURER_LIST)
course_3 = models.ChoiceField(choices=LECTURER_LIST)
# admin.py
class CourseAdmin(admin.ModelAdmin):
fieldsets = (
("강의 1", {"fields": (('lecturer_1', 'course_1'))}),
// 그룹 이름을 '강의 1'로 설정
// 강의 1에 해당하는 필드들을 개별 그룹으로 표시
// 필드들을 한 줄에 표시
("강의 2", {"fields": ('lecturer_2', 'course_2')})
// 그룹 이름을 '강의 2'로 설정
// 강의 2에 해당하는 필드들을 개별 그룹으로 표시
// 각 필드는 한 줄을 차지함
)
또한 fieldsets 내부의 딕셔너리의 값으로 "fields" 뿐만 아니라 다른 값을 추가로 사용할 수도 있다.
또한 fields나 fieldsets 옵션을 따로 명시하지 않았다면, 장고에서는 기본값으로 AutoField가 아니고 editable=True로 된 필드들만 관리자 페이지에 포함시킨다.
fieldsets, list_display의 차이점
헷갈리는 부분이라 적어보았다.
fields와 fieldsets는 처음 관리자 페이지에서 모델명을 누르면 나오는 조회 페이지가 아니라, 조회 페이지에서 개별 데이터를 누르면 나오는 추가 및 수정 페이지에서 어떤 필드를 표시할지를 결정한다.
반면 list_display는 관리자 페이지 시작 화면에서 모델명을 누르면 나오는 조회 페이지에서 어떤 필드를 표시할지를 결정한다.
8. filter_horizontal & filter_vertical
필드 이름 튜플을 값으로 받으며, ManyToManyField(다대다 필드) 에서만 작동한다.
fieldsets과 마찬가지로 조회 페이지가 아니라 추가 및 수정 페이지에서 작동한다.
many-to-many field 특성상 선택하는 가짓수가 많을 때는 다루기 어려울 수 있기 때문에, 간단한 인터페이스를 사용해서 데이터를 쉽게 추가 및 수정할 수 있게 했다.
filter_horizontal의 경우 선택되지 않은 가짓수(옵션)이 왼쪽, 선택된 옵션이 오른쪽 박스에 나타난다.
filter_vertical의 경우 선택되지 않은 옵션이 위쪽, 선택된 옵션이 아래쪽 박스에 나타난다.
9. form
관리자 페이지에서 모델뿐만 아니라 폼 데이터를 추가 및 수정할 때 사용한다.
# forms.py
class CarForm(forms.ModelForm):
class Meta:
model = Car
exclude = ['engine oil']
# admin.py
class CarAdmin(admin.ModelAdmin):
form = CarForm
object-relational-mapping 의 약자로, 객체(object)와 관계지향 데이터베이스(relational database)를 연결(mapping)하는 방법이다. 대부분의 프로그래밍 언어(python, java 등)에서는 객체 개념이 있고, 사용자 등 여러 모델을 만들 때 객체를 사용한다.
하지만 그 모델을 저장할 때는 DB(데이터베이스)에 저장하게 된다.
ORM은 프로그래밍 언어의 객체 개념을 관계지향 데이터베이스와 연결해 준다.
예를 들어 파이썬으로 프로젝트를 개발하는데 ORM이 없다고 가정해 보자.
그러면 프로젝트에서 유저 등 객체를 생성할 때 DB에 쿼리(query)를 날려야 하고, 파이썬 프로젝트 코드 내부에 직접 유저를 생성하는 SQL 코드를 입력해 줘야 한다.
그러나 ORM이 있다면 User.objects.create() 등의 간단한 파이썬 문법의 코드로 프로젝트 DB에 유저를 생성할 수 있다.
지금까지 장고에서 기본으로 제공하는 관리자 페이지를 보기 위해서 관리자 계정을 생성하고, 조회하는 방법까지 알아보았다.
그러나 장고는 관리자 페이지에 대해서 더 다양한 기능을 많이 제공하고, 관리자 페이지를 원하는 대로 직접 커스터마이징 할 수도 있다고 한다..!
개발을 하다보면 기존에 다른 사람들이 작업한 프로젝트를 이어받아서 작업해야 하는 경우도 생긴다.
이때 라이브러리 버전 관리, 환경변수 설정 등이 알맞게 되어야만 이전 사람들이 프로젝트를 진행하던 똑같은 환경에서 개발을 진행할 수 있다.
오늘은 간단하게 그 순서에 대해서 알아보겠다.
📒가상 환경
✅가상환경 만들기
우선 가상환경을 하나 만들어준다.
가상환경을 만들기 위해서는 virtualenv라는 라이브러리가 필요하다. 없다면 아래 커맨드를 이용해서 virtualenv 라이브러리를 설치하자.
이때 커맨드를 실행하는 위치는 프로젝트 root 디렉토리로 되어 있어야 한다.
pip install virtualenv
이제 가상환경을 만들어준다. 지금 만든 가상환경 안에서 프로젝트에 쓰일 라이브러리와 버전을 관리할 것이다.
virtualenv 뒤에는 가상환경의 이름을 붙여주면 된다. 보통은 venv를 많이 사용한다.
virtualenv venv // 가상환경 이름: venv
virtualenv myenv // 가상환경 이름: myenv
때때로 python 버전에 따라서 사용 가능한 라이브러리가 달라지기도 한다.
이 경우엔 가상환경을 만들 때 어떤 버전을 사용해서 가상환경을 운영할지도 따로 정할 수 있다.
virtualenv venv --python=3.8 // python 3.8을 사용하는 가상환경 설치
✅가상환경을 만드는 이유
가상환경(virtual environment)을 사용하는 이유는 여러 가지가 있을 수 있다. 대표적인 이유는 여러 개의 프로젝트를 동시에 진행할 때 라이브러리버전 관리가 쉽기 때문이다.
예를 들어 프로젝트 A는 django-rest-framework 0.0.1 버전을 사용하고 프로젝트 B는 0.0.2 버전을 사용할 경우, 버전이 다르기 때문에 프로젝트를 바꿔 가면서 작업할 경우 버전에 혼동이 생긴다. 심한 경우는 버전이 맞지 않아서 실행되지 않을 수 있다.
그러나 프로젝트별로 개별의 가상 환경을 만들고 그 안에서 작업하면, 가상환경마다 사용하는 라이브러리와버전이 다르고, 프로젝트별로 어떤 버전을 사용하건 다른 프로젝트에 영향을 주지 않는다.
✅가상환경 실행하기
위에서는 가상환경을 만들었다. 앞으로 프로젝트를 작업할 때마다 이 가상환경을 실행한 상태에서 작업해야 한다.
(가상환경에 프로젝트에 필요한 라이브러리가 모두 포함되기 때문에, 가상환경을 실행하지 않으면 어차피 프로젝트를 실행할 수 없다.)
윈도우에서 가상환경을 실행하는 방법은 Command Prompt에서 실행하는 방법과 PowerShell에서 실행하는 방법 두 가지가 있다.
Command Prompt(cmd 창)에서 실행하는 방법은, 가상환경 이름\Scripts\activate.bat
venv\Scripts\activate.bat
PowerShell에서 실행하는 방법은, .\가상환경 이름\Scripts\Activate.ps1
.\venv\Scripts\Activate.ps1
나는 항상 cmd로 작업해서 위의 방법을 사용했다. 그러나 powershell이 mac과 linux에서 사용하는 커맨드를 사용한다고 들어서, 앞으로는 powershell 명령어도 많이 익힐 필요가 있겠다.
아무튼 이 과정을 마치면 터미널 창에서 뜨던 디렉토리 앞에 (venv) 가 붙는다. 이는 가상환경이 현재 실행 중이라는 뜻이다. 이제 작업하면 된다!
라이브러리 설치
본인이 처음부터 끝까지 프로젝트를 만드는 경우라면 가상환경 작업을 끝내고 바로 프로젝트를 만들어도 되지만, 다른 사람들과 같이 개발하는 경우는 추가 작업이 필요하다.
✅현재 프로젝트 가상환경에서 사용한 라이브러리를 파일로 저장하기
여러 명이 프로젝트 하나를 개발하는 경우, 사람들이 작업하던 환경과 같은 환경에서 프로젝트를 개발해야 한다. 이때 라이브러리의 버전 관리 등을 쉽게 하기 위해서는 사용한 라이브러리의 버전을 모아서 관리할 필요가 있다.
이럴 때 pip의 freeze 기능을 사용한다.
pip freeze > requirements.txt
freeze를 하면 현재 프로젝트에서 사용된 라이브러리와 버전을 requirements.txt 파일을 만들어서 그 안에다 저장해 준다.
이러면 나중에 다른 사람이 이 프로젝트를 작업하게 되었을 때, requirements.txt 파일을 사용하여 똑같은 라이브러리와 버전을 가상환경에 설치하여 사용할 수 있다.
✅프로젝트에서 사용한 라이브러리를 설치하기
이 경우는 위의 경우와는 반대로, 다른 사람이 작업했던 프로젝트를 이어받아 작업할 때 사용한다.
앞서 작성된 requirements.txt 파일에 설치된 라이브러리와 버전을 하나하나 다운받지 않고, 다음 명령어를 사용한다.
(파일명을 다르게 하고 싶으면 뒤의 부분을 바꾸면 된다.)
pip install -r requirements.txt
requirements.txt 파일에 입력된 라이브러리와 버전을 현재 가상환경에 설치해 준다.
⚠️설정 파일(requirements.txt)은 딱 하나만 두고 관리하자.
여러 사람이 작업하다 보면 여러 개의 설정 파일이 생길 수 있다. 그러면 때론 파일을 혼동하여, 가장 최근의 라이브러리 설치 상태를 반영한 파일이 아닌 다른 파일에 작성된 라이브러리를 설치하게 될 수 있다. (내가 했던 실수...)
따라서 설정 파일(requirements.txt)은 꼭 하나만 두고 관리하자.
위의 두 과정에서 별다른 오류가 없었다면, 프로젝트 실행을 위한 기본적인 작업은 끝났다. 이제는 환경변수 작업만 남았다.
📒환경변수(.env)
프로젝트를 실행하기 위해서 필요한 중요한 정보를 담고 있는 변수.
보통 데이터베이스의 포트 번호, 비밀번호 등을 포함하기 때문에 절대 외부에 공유해서는 안 된다. (비밀번호 다 털림. 계정 털림.)
환경변수 파일 이름은 보통 .env 로 사용한다.
✅gitignore 사용해서 환경변수 파일이 github/git에 공유되지 않게 하기
이 파일을 공유하지 않으려면 gitignore를 활용해야 한다.
.gitignore 파일을 만들고, 이 안에다가 깃허브에 공유하고 싶지 않은 파일 이름 또는 확장자를 적어주면 된다.
예를 들어서, .gitignore 파일에 다음과 같이 입력해 보자.
.env
.venv
info.py
그리고 프로젝트를 깃허브에 등록할 때, 확장자가 env 또는 venv인 파일과 info.py 파일은 깃허브에 등록되지 않는다.
⚠️예외 경우
다만 예외인 경우도 있다. 이미 git에 등록되어 관리되던 파일이라면, 관리된 이후에 .gitignore 파일에 추가된다고 해서 업로드에서 제외되지 않는다.
이 경우 추가적인 git 명령어를 사용해서 작업해야 한다.
작업 과정
1. .gitignore 파일에 깃허브에 올리지 않을 파일명 추가
2. git 캐시 삭제
--cached 옵션을 추가하지 않으면 로컬 저장소에 있는 파일과 원격 저장소(깃허브)에 있는 파일이 모두 삭제된다.
지금은 원격 저장소인 깃허브에 등록된 파일만 삭제하려는 것이므로 꼭 --cached 옵션을 붙여주자.
지금까지는 스프링 빈을 수동으로 하나씩 등록하는 방법을 알아보았다. 그러나 등록할 빈의 개수가 많아질수록 단순 반복 작업이 될 수 있고, 누락할 수 있다는 문제점도 생긴다.
스프링에서는 자동으로 빈을 등록해주는 컴포넌트 스캔이라는 기능이 있다.
컴포넌트 스캔(Component Scan): 스프링 빈을 자동으로 끌어오는 기술.
코드로 보자.
AppConfig.java : 수동으로 스프링 빈을 등록한 클래스 파일
AutoAppConfig.java : 자동으로 빈을 등록할 클래스 파일
컴포넌트 스캔의 원리
@ComponentScan을 사용해서 컴포넌트 스캔을 할 수 있다.
@ComponentScan은 @Component이 붙은 클래스를 찾아서 전부 스프링 빈으로 등록한다.
@Configuration // 싱글톤 패턴으로 객체 생성
@ComponentScan // 컴포넌트 스캔 역할
public class AutoAppConfig {
// 별도의 코드 필요 X
}
@ComponentScan은 @Component 어노테이션만 스캔할까?
@Component 어노테이션은 다른 어노테이션 안에 붙어있기도 한다.
ex)
@Configuration 어노테이션을 타고 들어가면(원본 코드를 보면), @Component 어노테이션이 포함되어 있다. 이 경우 @Configuration이 붙은 클래스도 스프링 빈으로 등록된다. 물론 @Configuration 말고도 @Component을 포함하는 어노테이션들은 많고, 스프링 컨테이너는 이 어노테이션들이 붙은 클래스들도 모두 스프링 빈으로 등록한다.
@Component로 원하는 클래스를 스프링 빈으로 등록시키는 방법
등록하고 싶은 스프링 클래스 선언부 위에 @Component만 붙이면 된다.
@Component
public class SpringServiceImpl {
// code
}
⚠️인터페이스 말고, 구현 클래스의 선언부에다가 붙여야 한다.
지금까지 자동으로 스프링 빈을 등록해보았다.
이제는 의존관계를 자동으로 주입해 보자.
자동 의존관계 주입
@Autowired 라는 자동 의존관계 주입 어노테이션을 사용하면 된다.
@Autowired를 @Component이 붙은 클래스의 생성자 위에다가 붙인다.
(지금은 생성자 주입 방식을 사용해서 생성자 위에다가 붙일 건데, 다른 방법도 있다. )
@Component
public class SpringServiceImpl implements SpringService {
private final SpringRepository springRepository; // 해당 클래스가 의존하는 다른 클래스 객체
@Autowired
public SpringServiceImpl(SpringRepository springRepository) {
this.springRepository = springRepository;
}
}
그러면 스프링 컨테이너에서, @Component 가 붙은 스프링 빈들 중 SpringRepository 타입의 빈을 찾는다.
('타입'으로 조회한다.)
그리고 SpringServiceImpl이 호출될 때 그 찾은 스프링 빈을 매개변수로 넣어준다.
⚠️
@Autowired를 쓸 때에는 반드시 @Autowired를 사용한 생성자를 가진 클래스(SpringServiceImpl)와 해당 생성자가 필요로 하는 매개변수 클래스(여기서는 SpringRepository의 구현 클래스인 SpringRepositoryImpl)에 모두 @Component 어노테이션이 붙어 있어야 한다.
@Component
public class SpringRepositoryImpl implements SpringRepository {
// code
}
이제 테스트를 만들어 보자.
: AutoAppConfigTest.java
1) 스프링 빈을 불러오기 위해서 AnnotationConfigApplicationContext 객체를 생성하자.
2) 해당 객체 안에는 AppConfig 클래스 대신, 컴포넌트 스캔을 사용한 AutoAppConfig 클래스를 넣는다.
3) AutoAppConfig 클래스로 SpringService 타입의 스프링 빈을 조회할 수 있는지 확인하자.
public class AutoAppConfigTest {
@Test
void test() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
SpringService springService = ac.getBean(SpringService.class);
assertThat(springService).isInstanceOf(SpringService.class);
}
}
일부 클래스를 제외하고 스프링 빈을 등록할 수도 있다.
excludeFilters 옵션을 지정하면 된다.
세부 옵션으로는 type, classes 등 여러 가지가 있다.
@ComponentScan(
excludeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Service.class,
)
)
public class AutoAppConfig {
// no code
}
: @ComponentScan의 대상 중, Configuration어노테이션은 스프링 빈으로 포함시키지 않겠다는 의미.
❓자동 주입 방식에서 빈 이름을 정하는 방법
빈의 기본 이름은 맨 앞 글자만 소문자로 바뀐 클래스 이름 이다.
@Component
public class SpringRepositoryImpl implements SpringRepository {
// code
}
: 빈 이름은 springRepositoryImpl 으로 등록된다.
빈 이름을 지정할 수도 있다.
@Component("지정하고_싶은_이름")
@Component("spring_repo")
public class SpringRepositoryImpl implements SpringRepository {
// code
}
2. 탐색 위치와 기본 스캔 대상
컴포넌트 스캔을 할 때, 어느 위치부터 @Component 어노테이션을 탐색할지를 지정할 수 있다. : basePackages 옵션으로 지정할 수 있다.
@ComponentScan(basePackages = 'hello.core.member')
public class AutoAppConfig {
// code
}
: hello.core.member디렉토리 하위에 있는 파일들에서만 @Component를 탐색한다.
기본값
아무런 값도 없다면, basePackage의 기본값은 @ComponentScan이 붙은 클래스의 바로 위 패키지가 된다. ex)
AutoAppConfig.java 클래스의 바로 위 폴더가 hello.core이라면, : hello.core 아래의 파일들에 한해서 컴포넌트 스캔을 한다.
AutoAppConfig.class 는 hello.core 디렉토리 바로 아래에 위치한다. -> hello.core 패키지(디렉토리) 아래에 있는 파일들만 탐색한다.
@SpringBootApplication의 비밀
스프링 웹 어플리케이션을 실행할 때는 일반적으로 (패키지_이름)Application 클래스의 메인 메소드를 실행한다.
(여기서는 CoreApplication.java) 이 클래스는 기본적으로 최상단 패키지(여기서는 hello.core)의 바로 아래에 위치하는데, 그 이유가 있다.
@SpringBootApplication 안에는 @ComponentScan이 포함되어 있다. : 실제 웹앱을 만들 때는 스프링에서 @ComponentScan을 명시적으로 사용할 일이 거의 없다. 어차피 메인 메소드에 붙어있는 @SpringBootApplication 어노테이션 안에 포함되어 있기 때문이다!
그래서 웹앱을 실행하면, 프로젝트 폴더에 있는 모든 스프링 빈들을 자동으로 등록할 수 있었던 것이다. <-> 만약 @SpringBootApplication이 붙은 메인 클래스가 특정 하위 디렉토리에만 속해 있었다면, 모든 빈을 불러올 수 없었을 것이다.
컴포넌트 스캔의 대상
컴포넌트 스캔에서는 @Component만 스캔하지 않는다. 다른 어노테이션도 내부에 @Component를 포함하고 있기 때문이다. : @Controller, @Service, @Repository, @Configuration, ...
@Component을 상속하는 다른 어노테이션들
⚠️ 사실 어노테이션은 자바에서 지원하는 기능이 아니다. ⚠️ 한 어노테이션이 다른 어노테이션을 포함하는 상속 관계도 마찬가지다. 자바 언어는 어노테이션의 상속 관계 인식을 지원하지 않는다.
어노테이션의 상속관계
보통 한 클래스가 다른 클래스를 상속하면, 상속받은 클래스에서 지원하는 기능 + a 를 수행한다.
어노테이션도 마찬가지이다.
@Component는 @ComponentScan이 어떤 클래스를 스프링 빈으로 등록할지를 알려주는 역할을 한다.
그러나 @Component와 다른 어노테이션까지 같이 상속한 다른 어노테이션은 그 외의 부가적인 기능도 함께 제공한다.
1) @Controller
스프링 MVC 컨트롤러로 인식한다.
2) @Repository
스프링에서 데이터로 접근하는 클래스로 인식한다. 또한 데이터 계층에서 예외가 발생했을 때, 이를 스프링 예외로 변환시킨다. ✖️ 스프링 예외로 변환시킨다 = 예외를 추상화시킨다
3) @Service
특별한 처리를 하지 않는다. 개발자들이 비즈니스 계층을 인식하는 데 도움이 된다.
3. 필터
컴포넌트 스캔에서 추가할 대상과 제외할 대상을 설정할 수 있다. : includeFilters, excludeFilters 옵션 사용
추가할 어노테이션, 제외할 어노테이션을 만들고 필터가 작용하는지를 직접 확인하기 위해 어노테이션을 만들어 보자.
✅어노테이션을 만드는 방법
1) 인터페이스를 선언하고, 인터페이스 키워드 앞에 @을 붙인다.
public @interface IncludeAnnotation {
}
2) 해당 인터페이스 위에다 추가로 어노테이션을 더 설정할 수 있다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IncludeAnnotation {
}
➕
@Target : 해당 어노테이션이 어느 타입에 붙을지를 지정.
ElementType.TYPE : 클래스 단위에 지정하는 어노테이션. TYPE이 아닌 다른 값으로 지정하면 어노테이션이 필드나 생성자에 붙게 할 수도 있다.
(ex. @Autowired)
방금은 컴포넌트 스캔에 추가할 어노테이션을 만들었다. 이제 제외할 어노테이션을 만들어 보자.
: 같은 방법으로 ExcludeAnnotation을 만들자.
테스트하기
위에서 만든 @IncludeAnnotation, @ExcludeAnnotation을 사용해서 테스트를 해 보자.
1. BeanInclude와 BeanExclude 클래스를 만들고, 각각 @IncludeAnnotation, @ExcludeAnnotation을 붙이자. 2. 테스트를 만들어 보자: ComponentFilterAppConfigTest.java 목적: includeFilter, excludeFilter 옵션이 잘 작동하는지 확인.
1) 테스트 클래스 안에 static class를 만들고, 해당 클래스를 @Configuration을 붙여서 스프링 빈 정보를 담는 클래스로 선언한다. 2) 해당 클래스를 참고한 AnnotationContextApplicationConfig 객체를 만들고, 빈을 조회한다. 3) 이때 BeanInclude(includeFilter)를 조회하면 조회가 되어야 한다. 4) 반면 BeanExclude(excludeFilter)를 조회하면 에러(NoSuchBeanDefinitionException)가 발생해야 한다.
➕
에러는 최대한 구체적으로 명시하는 것이 좋은 테스트이다. <-> Exception.class -> 별로 도움 안 됨!
4. 중복 등록과 충돌
스프링 빈을 등록하는 방법으로 수동(AppConfig 클래스에 직접 등록)과 자동 등록(@ComponentScan 이용)이 있었다.
수동 등록에서는 당연히 같은 이름의 빈이 2개 이상 있으면 충돌이 발생했다. 그렇다면 자동 등록에서는 어떨까?
1) 자동 vs 자동
: ConflictingBeanDefinitionException이 발생한다.
2) 자동 vs 수동
: 오류가 나지 않는다. 수동 등록 빈이 자동 등록 빈을 오버라이딩(덮어 씌우기) 했기 때문이다. ⚠️ 이름이 같을 경우, 자동 등록 빈보다 수동 등록 빈이 우선권을 가진다.
그러나 이런 설정은 대부분 개발자의 의도적인 테스트에 의해 발생하기보다는, 수많은 설정이 꼬여서 발생하는 것이 대부분이다.
따라서 스프링은 이후 자동 등록된 빈과 수동 등록한 빈의 이름이 같을 때, 어플리케이션을 종료(disable)시키는 방향으로 업데이트 되었다.
package hello.core.singleton;
public class StatelessService {
public int order(String name, int price){
System.out.println("name = " + name + ", price = " + price);
return price;
}
}
예시 코드에서, app.get() 라우터 미들웨어가 실행되기 전에 이미 body-parser 미들웨어가 실행되었고, 그 미들웨어는 filelist 변수에 실행값을 리턴한 상황이다.
따라서 filelist 변수의 값을 request 객체의 list 속성에 추가하면(list 속성은 원래 있던 속성이 아니고, 임의로 추가한 속성이다), 후에 request 객체의 list 속성에 접근해서 body-parser 미들웨어가 추출한 값을 사용할 수 있다.
[2] compression
웹서버가 클라이언트의 요청에 응답할 때 그 응답을 압축해서 보내는 것을 도와주는 미들웨어이다. (데이터의 양이 많으면 http 요청으로 보내기 어려울 수 있다.)
압축된 데이터는 기존 데이터에 비해서 크기가 작고, '개발자 도구'의 '네트워크' 탭에서 보면 어떤 방식으로 압축되었는지도 알 수 있다. 클라이언트(브라우저)는 해당 데이터를 받아서, 그 데이터가 압축된 방식을 사용해서 반대로 그 데이터를 해제(=압축 풀기)한 다음에 원본 파일을 볼 수 있다.
+미들웨어 만들기
직접 미들웨어를 만드는 것도 가능하다. 미들웨어는 함수이다. 다만 request, response, next 라는 3개의 매개변수를 받아야 한다.
app.use((req, res, next)=>{
});
이렇게 입력해 주면 미들웨어를 만들고 만든 미들웨어를 앱 어플리케이션에 장착한 것이다. (=바로 사용할 수 있다)
req(request): 클라이언트가 보낸 요청
res(response): 서버가 보낼 응답
next(): 해당 미들웨어의 실행이 끝난 다음에 불러올 미들웨어이다. 서버의 응답을 해당 미들웨어에서 끝낼 것이 아니라면, 반드시 next()를 통해 다음 미들웨어를 호출해 주어야 한다.
2. Routing(라우팅)
1) 정의
데이터 통신에서의 라우팅(Routing)이란네트워크상에서 주소를 이용하여 목적지까지 메시지를 전달하는 방법을 체계적으로 결정하는 경로선택 과정을 말한다. 이 과정을 능동적으로 수행하는 장치를 라우터(Router)라고 한다.
즉 웹 서버로 http 요청이 들어오면, 해당 요청의 url 주소를 이용하여 요청을 체계적으로 처리하는 과정을 라우팅이라고 한다. 또한 이를 처리하는 미들웨어를 Router(라우터)라고 한다.
app.get('/:parameter-이름', (req, res)=>{ res.send(req.params); }); : 후에 localhost:port-번호/parameter-값 url로 들어가면, 요청(request)의 매개변수(parameter/params)속성으로 json 타입의 객체가 반환되는 것을 볼 수 있다.
이렇게(↓). {"parameter 이름": "parameter 값"}
res.send() 또는 res.end()는 말 그대로 서버에서 응답을 종료한다는 의미이다.
클라이언트에서 요청을 보내면 서버가 요청에 대해 응답하는 방식으로 통신이 일어난다.
요청이 원활하게 이뤄지려면 서버는 응답을 완료한 뒤 응답이 끝났다고 명시, 표현해 줘야 한다.
그 표시가 res.send() 또는 res.end() 메소드이다.
만약 응답이 끝냈다고 명시해주지 않으면, 응답이 완료되지 않고 쭉 늘어지는 상황(hanging)이 발생한다.
3) Router(라우터) - 주소 체계의 변경
복잡한 웹 서비스의 경우 수백 개의 api가 존재할 수 있는데, 그 api를 하나의 파일에 담는 것은 무리이다. 여러 개의 api를 적절한 경로로 나눠서 복잡한 구조를 간단하게 하고 싶을 때 라우팅 작업을 사용한다.
라우팅 처리하는 순서:
1. 공통된 url 경로를 가진 미들웨어들을 별도의 파일에 분리한다. 2. app.js 파일(웹 어플리케이션이 실행되는 메인 역할의 파일)에다가 1번 파일의 주소를 변수로 불러온다.
4. 1번 파일에다가 express.Router(); 값을 갖는 변수를 선언한다. 이 변수가 라우터 역할을 할 것이다.
const express = require('express'); // router 사용하기 위해서 필요한 express module 불러오기
const router = express.Router();
5. 1번 파일의 각 api(라우터 역할을 하는 미들웨어)에서 app으로 시작하는 미들웨어의 시작 부분을 2번 변수의 이름으로 바꾼다.
(1~5번을 합한 예시)
# 공통된 url 경로를 가진 미들웨어들을 분리한 별도의 파일
const express = require('express');
const router = express.Router();
// 다른 미들웨어(express 공식 미들웨어/제 3자가 작성한 미들웨어)
router.get('/', (req, res, next)=>{
// 미들웨어 코드
});
// 내가 작성한 기타 미들웨어 코드
module.exports = router;
# app.js 파일
const express = require('express');
const app = express();
const exampleRouter = require('./example/path');
app.use('/example/path', exampleRouter); // 다른 파일에서 사용한 라우터를 호출하는 코드
// app.js에 명시된 다른 미들웨어들
*단, app.js 파일에서도 미들웨어는 위에서부터 호출되기 때문에, app.use()로 다른 파일에서 사용한 라우터를 호출하는 코드는 위쪽에 배치되어야 한다.
+
redirection 메소드: 주어진 url에 해당하는 페이지로 단순 이동하는 역할을 한다.
: response.redirect('이동하려는 url');
response.redirect('/example/url');
3. 에러 처리
express에서 에러를 처리하는 방법은 크게 두 가지가 있다.
1. 코드의 맨 아래에다 에러를 처리할 미들웨어 작성하기.
미들웨어는 순서대로 실행되므로, 반드시 파일의 끝 부분에 작성해야 함!
이때 해당 미들웨어는 매개변수로 (err, req, res, next) 네 개의 매개변수를 받는다.
(express에서는 그래야만 해당 미들웨어가 에러를 처리하는 역할을 하도록 정해 놓았다.)