Git: clone, single-branch, checkout

 

clone

보통 공동으로 작업할 때 깃(git)을 많이 사용하는데, 깃에는 두 개의 저장소 개념이 있다. 

사용자의 PC로컬 저장소, 깃허브 리포지토리원격 저장소라고 한다. 

git clone 명령어를 사용하면 원격 저장소에 저장된 코드를 로컬 저장소로 가져올 수 있다. 

git clone {리포지토리 url(http)}

 

리포지토리 url은 깃 리포지토리 화면에서 code 버튼을 눌러서 얻을 수 있다. 

지금은 http url을 기준으로 리포지토리를 가져오는 방법을 알아보았지만, 나중에는 ssh를 기준으로 리포지토리를 가져오는 방법도 알아봐야겠다. 

 

 

branch

git에는 브랜치(branch)라는 개념이 있다. 하나의 나무에서 여러 개의 가지(branch)가 나온다고 생각하면 편하다.

기본 중심은 master 브랜치이다.

다른 브랜치끼리 서로 합칠 수도 있고, 하나의 브랜치에서 다른 브랜치를 만들어 낼 수도 있다. 

 

여러 개의 브랜치를 만들지 않고 모두가 하나의 브랜치에서 작업한다고 가정해 보자.

깃허브에 작업 내용을 반영할 때, 각자의 코드가 다른 부분이 생기면 그 때마다 충돌이 발생한다.

물론 나중에는 불일치를 해결해야 하겠지만, 깃허브에 코드를 올릴 때마다 이 충돌을 해결하는 것은 번거로울 수 있다. 

 

하지만 브랜치가 있다면 각자의 브랜치에서 작업하고, 나중에 여러 개의 브랜치를 하나로 합칠 때만 이 충돌을 해결해 주면 된다.

따라서 깃허브에 코드를 올릴 때마다 충돌이 발생할 걱정이 없다. 

 

이처럼 브랜치는 하나의 프로젝트를 여러 목적에 따라서 나눌 때(개발용 브랜치, 실제 배포용 브랜치, 테스트용 브랜치 등등), 또는 사용자마다 다른 브랜치를 사용한 다음 하나로 합칠 때도 사용한다. 

 

 

single-branch

위에서 clone으로 깃허브의 코드를 가져올 때, 해당 리포지토리에는 여러 개의 브랜치가 있을 수 있다. 

이때 특정 브랜치의 코드만 가져오고 싶을 때 사용하는 것이 single-branch이다. 

 

즉 single-branch를 사용하면 원격 저장소에서 전체를 다운받는 것이 아니라, 개별 브랜치의 내용만 가져오는 것이 가능하다. 

git clone -b {브랜치 이름} --single-branch {리포지토리 url}

 

 

checkout

single-branch와 달리, 이번에는 리포지토리의 모든 브랜치의 코드를 가져왔다고 해 보자. 

이때 때로는 작업하는 브랜치를 바꿔 가면서 개발해야 하는 상황도 있다. 

checkout을 사용하면 필요할 때마다 현재 작업하는 브랜치를 바꾸면서 작업할 수 있다. 

참고로 checkout의 기본 브랜치는 origin, 즉 master 브랜치이다. 

git checkout {브랜치 이름}

 

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

Mac 환경설정  (0) 2024.07.15
Software Release Life Cycle  (0) 2023.07.15
OAuth 2.0 기본원리  (0) 2022.09.26
인증(Authentication)  (0) 2022.07.14
linux: cron 사용해서 자동으로 스케줄 실행하기  (0) 2022.07.09

관리자 페이지에서는 사용자가 정의한 모델에 대한 기본적인 CRUD 기능을 제공한다. 

그러나 때로는 사용자가 직접 원하는 기능을 추가하고 싶을 수 있다. 

이를 위해서 장고에서는 어드민(관리자) 페이지 커스터마이징 기능도 제공한다. 

 

관리자 페이지 커스터마이징 작업은 admin.py 파일에서 이루어진다. 

우선 각 모델에 해당하는 모델 관리자 클래스를 만들어 놓자. 

 

대략적으로 admin.py 에서 정의한 모델 클래스는

 

추가적인 옵션

class ObjectAdmin(매개변수, 여러 개가 들어갈 수도 있다):

    필드들(optional)

    함수들(optional)

 

이렇게 구성된다. 

 

 

admin.ModelAdmin

장고 어드민 인터페이스의 구현체라고 한다. 

정확히 무슨 말인지는 모르겠다!

[이해하면 포스팅하기]

 

 

register

modelAdmin을 등록하기 위해서는 register 선언을 해 주어야 한다. 

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.pyINSTALLED_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):
	list_display = {'id', 'view_username', 'view_userid'}

	@admin.display(empty_value="no name")
	def view_username(self, obj):
    	return obj.username
        
	@admin.display(empty_value="no id")
	def view_userid(self, obj):
		return obj.userid

 

5. exclude

필드 리스트를 값으로 받는다. 

제외하고 싶은 필드들이 있을 때, exclude의 값으로 입력한다. 

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" 뿐만 아니라 다른 값을 추가로 사용할 수도 있다. 

대표적으로는 classes, description 등이 있다. 

 

1) classes

해당 fieldset에 CSS 속성을 적용할 때 사용한다. 

class CourseAdmin(admin.ModelAdmin):
	fieldsets = (
		(None, {
        "fields": (('lecturer_1', 'course_1')),
        "classes": ('wide', 'extrapretty')
        }),
    )

 

2) description

각 fieldset 위에 추가 텍스트를 넣을 때 사용한다. 

 

또한 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

 

위의 경우, admin-form-model이 연결되어 데이터를 수정 및 변경할 수 있다. 

 

 

참고한 포스트

Admin actions | Django documentation | Django (djangoproject.com)

The Django admin site | Django documentation | Django (djangoproject.com)

장고 마스터하기 - 5장 - 김땡땡's blog (yonghyunlee.gitlab.io)

 

대부분의 웹 서비스에는 관리자 페이지가 있다. 관리자 페이지에서는 가입한 회원과 관련된 데이터들을 조회할 수 있다. 이를 통해 회사는 서비스가 어떻게 운영되고 있는지도 판단할 수 있기 때문에, 대부분의 웹 서비스에는 관리자 페이지가 있다. 

장고(django)에서도 관리자 페이지 기능을 제공한다. 

 

python manage.py runserver 명령어로 서버를 띄우면 기본 주소인 http://127.0.0.1:8000(포트번호) url로 로컬 서버에 접속할 수 있다. 

 

관리자 페이지를 보려면 http://127.0.0.1:8000/admin url로 접속하면 된다. 

그러면 관리자 페이지를 보기 위해서 관리자 아이디와 비밀번호를 입력하라는 창이 뜬다. 

 

관리자 계정 생성하는 방법

관리자는 보통 다른 사용자(유저)들의 정보를 열람, 수정 및 삭제할 수 있는 권한을 가진다. 따라서 장고에서도 관리자를 user이 아니라 superuser 이라고 부른다. 

 

관리자 계정으로 로그인하려면 우선 관리자 계정을 생성해야 한다. 

python manage.py createsuperuser

 

그러면 이메일, 이메일 주소, Password, Password(again) 을 입력하라는 메시지가 뜬다. 이에 맞게 입력해 주면 된다. 

나중에 관리자 계정으로 로그인할 때는 이메일과 Password를 입력해야 하니 이 두 정보는 꼭 기억하자!

 

Superuser created successfully.

이 메시지가 뜨면 성공적으로 관리자 계정이 만들어진 것이다. 

 

그럼 이제 로그인을 해 보자. 

python manage.py runserver

 

http://127.0.0.1:8000/admin url을 입력하면 로그인 화면이 나온다. 

이메일 에는 앞서 입력한 이메일 정보를, 비밀번호에는 앞서 입력한 Password 정보를 입력하면 로그인이 된다. 

 

그러면 관리자 페이지가 뜨고, 지금까지 프로젝트 내에서 정의한 모델이 있다면 각 모델에 데이터를 추가하거나 변경할 수 있다. 

 

관리자 계정 삭제하는 방법

만약에 관리자 계정의 정보를 잘못 입력하거나 새로운 관리자를 만들고 싶다면 기존 계정을 삭제하면 된다. 

관리자 계정도 결국은 하나의 사용자이기 때문에, 프로그램 내부에서 사용자를 조회하고 superuser인 유저를 삭제하는 방식으로 관리자 계정을 삭제하면 된다. 

 

우선 Shell에 접속한다. 

python manage.py shell

 

Python Shell 에서는 파이썬 문법의 코드를 사용해서 프로젝트의 DB에 저장된 데이터를 조회, 삭제 및 변경할 수 있다. 장고 ORM이 제공하는 기능 중 하나이다. 

파이썬 쉘(shell)이 아니었다면 직접 DB에 들어가서 SQL문으로 데이터를 조회해야 한다. 

 

여기서 확인해야 할 것이 있다. 

보통 관리자 계정은 django.contrib.auth.models.User 의 인스턴스로 저장된다. 

그러나 settings.py 파일의 AUTH_MODELS 필드가 위 클래스가 아닌 다른 클래스로 되어 있다면 관리자 계정이 AUTH_MODELS에 해당하는 클래스의 인스턴스로 저장된다. 

그러므로 settings.py의 AUTH_MODELS 필드가 어떤 클래스로 지정되어 있는지 먼저 확인하자. 

 

내 프로젝트의 경우는 사용자가 직접 만든 account.User 모델로 되어 있었다. 

이 경우, django.contrib.auth.models.User 모델을 import 해서 관리자 계정을 조회하면 다음과 같은 오류가 발생한다. 

즉 관리자를 조회하는 데 사용한 클래스와 AUTH_MODELS 의 클래스가 다를 때 이런 오류가 발생하는 것이다. 

 

User.objects.get(username="", is_superuser=True)

 

AUTH_MODELS 에 입력된 클래스를 import 하고 관리자 계정을 조회해 보자. 

이때 다른 기존의 유저들이 등록되어 있는 상황이라면, 헷갈리지 않기 위해서 is_superuser=True 를 옵션으로 추가한다. 

만약 관리자 계정이 나온다면, 이 계정을 제거해 주면 된다. 

User.objects.get(username="", is_superuser=True).delete()

 

그리고 위의 과정을 통해 다시 관리자 계정을 생성하면 된다. 

 

🗒️ORM이란?

object-relational-mapping 의 약자로, 객체(object)와 관계지향 데이터베이스(relational database)를 연결(mapping)하는 방법이다. 대부분의 프로그래밍 언어(python, java 등)에서는 객체 개념이 있고, 사용자 등 여러 모델을 만들 때 객체를 사용한다. 

하지만 그 모델을 저장할 때는 DB(데이터베이스)에 저장하게 된다. 

ORM은 프로그래밍 언어의 객체 개념을 관계지향 데이터베이스와 연결해 준다. 

 

예를 들어 파이썬으로 프로젝트를 개발하는데 ORM이 없다고 가정해 보자.

그러면 프로젝트에서 유저 등 객체를 생성할 때 DB에 쿼리(query)를 날려야 하고, 파이썬 프로젝트 코드 내부에 직접 유저를 생성하는 SQL 코드를 입력해 줘야 한다. 

그러나 ORM이 있다면 User.objects.create() 등의 간단한 파이썬 문법의 코드로 프로젝트 DB에 유저를 생성할 수 있다. 

 

 

지금까지 장고에서 기본으로 제공하는 관리자 페이지를 보기 위해서 관리자 계정을 생성하고, 조회하는 방법까지 알아보았다. 

그러나 장고는 관리자 페이지에 대해서 더 다양한 기능을 많이 제공하고, 관리자 페이지를 원하는 대로 직접 커스터마이징 할 수도 있다고 한다..! 

다음 번에는 장고의 관리자페이지 커스터마이징 방법에 대해서 작성해 보겠다. 

 

 

참고한 포스트

Writing your first Django app, part 2 | Django documentation | Django (djangoproject.com)

장고(django)에서 superuser를 삭제하는 방법. : 네이버 블로그 (naver.com)

 

개발을 하다보면 기존에 다른 사람들이 작업한 프로젝트를 이어받아서 작업해야 하는 경우도 생긴다. 

이때 라이브러리 버전 관리, 환경변수 설정 등이 알맞게 되어야만 이전 사람들이 프로젝트를 진행하던 똑같은 환경에서 개발을 진행할 수 있다.

오늘은 간단하게 그 순서에 대해서 알아보겠다. 

 

📒가상 환경

가상환경 만들기

우선 가상환경을 하나 만들어준다. 

 

가상환경을 만들기 위해서는 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 옵션을 붙여주자. 

git rm -r --cached .

3. git에 현재 작업한 내용을 임시로 저장

: add 뒤에는 현재 디렉토리를 명시하기 위해서 . 하나를 붙인다. 

git add .

4. 현재 git 프로젝트의 상태 확인

: 현재 등록된 커밋(commit)이 있는지를 보여준다. 

git status

5. 변경사항 저장하기

git commit

 

.env 파일 간단하게 생성하는 방법

gitignore.io - 자신의 프로젝트에 꼭 맞는 .gitignore 파일을 만드세요 (toptal.com)

이 링크에서 본인의 운영체제와 사용하는 언어/프레임워크를 검색어로 입력하면 그에 맞는 .env 파일 형식을 생성해 준다. 

이를 참고하여 제외할 부분은 제외하고, 추가할 부분은 추가로 작성하는 방식으로 .env 파일을 작성할 수 있다. 

 

📒Git(깃)

Git은 컴퓨터 파일의 변경사항을 추적하고 여러 명의 사용자들 간에 해당 파일들의 작업을 조율하기 위한 분산 버전 관리 시스템다.

 

로컬 저장소와 원격 저장소

여러 명의 사용자는 각자의 로컬 PC에서 작업을 진행하고, 이를 원격 저장소에 공유하는 방식으로 작업한다. 

 각자의 PC=로컬 저장소, Github 계정=원격 저장소 이다. 

 

파일 업로드 과정

개인 PC에서 작업한 이후에는 작업내용을 공동 저장소인 깃허브에도 저장해야 한다. 

이 과정을 보통 '커밋을 한다'고 하지만, 구체적으로는 네 가지 상태로 나눠진다. 

 

0) Working Directory

작업하면서 생긴 변경사항들이 아무 영역에도 반영되지 않은 상태이다. 

 

1) Staging

작업하면서 생긴 변경사항이 '임시저장' 된 단계이다. Github Desktop 등의 프로그램을 사용하면 변경사항이 생기자마자 바로 staging 영역으로 등록된다. 

그렇지 않은 경우, git add . 명령어를 사용해서 현재 디렉토리에서 발생한 변경사항을 임시저장인 staging 영역으로 등록해 준다. 

 

2) Commit

Staging 영역으로 등록된 변경사항들이 기록으로 남는 영역이다. 아직 깃허브에 변경사항이 저장된 상태는 아니다. git commit 명령어를 통해 실행할 수 있다. 

다만 Staging 단계와의 차이점은 Commit부터는 깃허브에서 작업 기록으로 남는다는 것이다. Staging 단계에서는 add를 남발해도 임시저장의 개념이라 아무런 기록이 남지 않지만, commit을 남발하면 그 변경사항이 모두 기록된다. 

따라서 보통의 공동작업에서는 커밋을 남발하지 않고, 기능 추가/기능 개선/오류 처리 등 여러 기준을 세우고 그에 따라서 커밋을 하는 것이 일반적이다. 

 

3) Push

commit 된 사항에 대해서 push를 하도록 권장한다. push 된 내용은 깃허브에 반영될 수 있다. 

git push 명령어를 통해 실행할 수 있다.

다만 프로젝트에 따라서 개인이 push하면 바로 깃허브에 반영이 되도록 할 수도 있고, 여러 사용자가 확인 뒤 승인(?)하면 변경사항이 반영되도록 할 수도 있다. 

 

 

참고한 포스트

[Git] gitignore란 무엇일까? :: Gyun's 개발일지 (tistory.com)

git rm --cached 파일 삭제 :: 마이구미 :: 마이구미의 HelloWorld (tistory.com)

[GIT] area, add, commit, push, stage (velog.io)

 

 

이 포스트는 인프런 김영한 님의 '스프링 핵심 원리 - 기본편' 강의를 들으면서 내용을 정리한 글입니다. 

 

[메인 컨텐츠]

스프링 핵심 원리 - 기본편 대시보드 - 인프런 | 강의 (inflearn.com)

 

📅 2022-05-23 

ℹ️ 목차

1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

2. 탐색 위치와 기본 스캔 대상

3. 필터

4. 중복 등록과 충돌

 


1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

컴포넌트 스캔의 필요성

지금까지는 스프링 빈을 수동으로 하나씩 등록하는 방법을 알아보았다. 
그러나 등록할 빈의 개수가 많아질수록 단순 반복 작업이 될 수 있고, 누락할 수 있다는 문제점도 생긴다. 

 

스프링에서는 자동으로 빈을 등록해주는 컴포넌트 스캔이라는 기능이 있다. 

 

컴포넌트 스캔(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)시키는 방향으로 업데이트 되었다. 

 

disable 말고 overriding 시키는 방법

main>java>resources>application.properties

spring.main.allow-bean-definition-overriding=true

: 수동과 자동 등록된 빈 2개가 이름이 같을 때 overriding이 발생한다. 

(default : False)

 

 

싱글톤 방식의 주의점

 

무상태(stateless)로 설계해야 하는 이유

웹 어플리케이션 설계에서 유용한 싱글톤 방식을 사용할 때는 싱글톤 클래스를 반드시 무상태로 설계해야 한다. 다음 예제를 보자.

 

: StatefulService.java

: StatefulServiceTest.java

 

해당 코드에서는 ThreadA에서 userA가 주문을 하고, 가격을 확정하기 전에 ThreadB에서 userB가 다른 상품을 주문한다.

싱글톤 클래스라 클래스 안의 필드도 공유하고 있기 때문에, 이 경우 userA가 주문했던 금액과는 다른 금액이 나오게 된다.

무상태로 설계하기 위해서는 기존의 클래스 안의 멤버 변수(필드)를 지역 변수로 바꾸거나, ThreadLocal을 사용해야 한다.

 

지역 변수를 사용한 예시

StatelessService.java

더보기
package hello.core.singleton;

public class StatelessService {

    public int order(String name, int price){
        System.out.println("name = " + name + ", price = " + price);
        return price;
    }

}

StatelessServiceTest.java

더보기
public class StatelessServiceTest {

    @Test
    @DisplayName("싱글톤 클래스를 무상태로 설계해야 한다.")
    void statelessServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatelessService statelessService1 = ac.getBean("statelessService", StatelessService.class);
        StatelessService statelessService2 = ac.getBean("statelessService", StatelessService.class);

        int priceA = statelessService1.order("userA", 10000);
        int priceB = statelessService2.order("userB", 20000);

        assertThat(priceA).isEqualTo(10000);
        assertThat(priceB).isEqualTo(20000);

    }

    @Configuration
    static class TestConfig {

        @Bean
        public StatelessService statelessService() {
            return new StatelessService();
        }
    }

}

 

ThreadLocal이란?

스레드(Thread)의 개념

프로그램을 실행할 때, 스레드(thread) 프로세스(process)라는 말을 사용한다.

 

 프로세스: 운영체제로부터 자원을 할당받는 작업의 단위.

여러 개의 어플리케이션을 사용한다면, 여러 개의 프로세스(multi-process)가 동작 중이다.

 

 스레드: 프로그램(프로세스) 실행의 단위.

하나의 어플리케이션(프로세스)를 실행할 때도 여러 개의 스레드(multi-thread)를 사용할 수 있다.

 

 멀티 프로세스 -> 멀티 스레드

 멀티 스레드 -> 멀티 프로세스

 

변수의 생성 영역(메모리)

객체나 변수를 생성할 때 Heap영역, 또는 Stack영역에 위치시킬 수 있다.

 

 Heap: 모든 스레드에서 공유하는 영역.

ex) 정적(static) 변수

 Stack: 하나의 스레드에서 사용하는 영역.

ex) 지역(local) 변수

 

ThreadLocal의 구조

ThreadLocal스레드 정보를 key로 사용하여 값을 저장하는 Map 구조를 갖고 있다.

멀티 스레드 환경에서 Stateful 클래스를 싱글톤으로 선언하고 싶을 때, ThreadLocal을 사용하면 각 스레드 별로 다른 변수를 사용할 수 있다.

 

ThreadLocalService.java

더보기
package hello.core.threadlocal;

public class ThreadLocalService {

    private static ThreadLocal<String> product = new ThreadLocal<>();
    private static ThreadLocal<Integer> price = new ThreadLocal<>();

    public ThreadLocalService() {}

    public void setProduct(String item) {
        product.set(item); // set()으로 value 값 지정 가능
    }

    public void setPrice(Integer value) {
        price.set(value);
    }

    public void printOrder() {
        System.out.println("product = " + product.get() + ", price = " + price.get());
				// get()으로 value 값 반환 가능
    }
}

ThreadLocalServiceTest.java

더보기
package hello.core.threadlocal;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


public class ThreadLocalServiceTest {

    @Test
    void threadLocalOrder() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        ThreadLocalService tls = ac.getBean("threadLocalService", ThreadLocalService.class);

        Integer priceA = 10000;
        Integer priceB = 20000;
        String productA = "strawberry";
        String productB = "watermelon";

        Thread threadA = new Thread(()->{
            tls.setPrice(priceA);
            tls.setProduct(productA);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tls.printOrder();

        });

        Thread threadB = new Thread(()->{
            tls.setPrice(priceB);
            tls.setProduct(productB);
            tls.printOrder();
        });

        threadA.start();
        threadB.start();

    }

    @Configuration
    static class TestConfig {

        @Bean
        ThreadLocalService threadLocalService() {
            return new ThreadLocalService();
        }
    }

}

 

ThreadLocal 사용할 때 주의점

⚠️ 메모리 문제

ThreadLocal은 사용 시 메모리를 관리해야 한다.

ThreadPool을 사용하면 기존에 생성했던 ThreadLocal을 재사용할 수 있는데, 이때 이전에 삭제되지 않은 메모리가 남아있을 수 있다.

ThreadPool을 사용할 경우 반드시 ThreadLocal.remove()를 이용하여 남은 데이터를 제거해 주어야 한다.

 

*참고한 포스트들*

 

@Configuration과 싱글톤

스프링 컨테이너는 @Configuration 으로 등록한 클래스에서 빈을 생성 및 조회하는데, 이 빈들을 모두 싱글톤 타입으로 만든다고 했다.

그런데 @Configuration으로 등록된 AppConfig 클래스의 각 빈 메소드에서는 new() 생성자로 객체를 생성한다.

어떻게 각 빈을 호출하면서 각 객체는 한 번만 생성하는 것이 가능할까?

 

: AppConfig.java

 

테스트를 위해서 각 구현 클래스(MemberServiceImpl.java, OrderServiceImpl.java)에 MemberRepository 객체를 조회할 수 있는 클래스를 임시로 작성하고, 결과를 보자.

 

: ConfigurationSingletonTest.java

 

모든 MemberRepository 객체의 주소값이 같다.

즉 MemberRepository 객체는 딱 한 번만 생성되고, 다른 클래스는 오직 하나의 객체를 공유하고 있다.

⇒ 싱글톤 패턴을 잘 준수한다.

 

AppConfig.java 파일을 변경하여, 각 빈이 호출될 때마다 로그를 남겨서 확인해 보자.

결과는 다음과 같다.

빈이 한 번씩만 호출되었다. 스프링 컨테이너는 인스턴스를 한 번 생성하고 나면 해당 빈 메소드를 더 이상 호출하지 않은 것이다.

 

@Configuration과 바이트코드 조작

빈이 한 번씩만 호출된 이유는 스프링 컨테이너가 바이트 코드를 조작해서, 사용자가 입력한 @Bean과는 다른 클래스를 만들고 그 클래스를 통해 싱글톤 패턴을 사용하고 있기 때문이다.

 

: ConfigurationTest.java

 

스프링 컨테이너는 싱글톤 패턴을 유지하기 위해서 사용자가 @Configuration에 등록한 코드를 조작해서 새로운 클래스를 만든다. 그 클래스가 스프링 빈으로 등록된 것이다.

 

바이트 코드 조작

바이트 코드란?

 바이트 코드

고급 언어(프로그래밍 언어. 자바 언어)로 작성된 코드를 가상머신(여기서는 JVM)이 이해할 수 있는 중간 단계의 코드로 컴파일한 언어.

기계어보다는 추상적(high-level)이고 고급 언어보다는 low-level이다.

 

코드 조작이 가능한 이유

🔑 바이트 코드를 조작하면 실행 결과가 달라지는 이유

자바 코드는 1차로 JVM이 처리할 수 있는 바이트 코드로 변환된다. 그리고 그 바이트 코드를 읽어서 비로소 프로그램이 실행된다.

바이트 코드 내용대로 실행이 되기 때문에, 중간 단계에서 코드가 조작된다면 결과값도 바뀌게 된다.

참고한 포스트: [Java] 바이트코드 조작 (tistory.com)

 

코드를 조작하는 방법(예시)

💡 byteBuddy 라이브러리

바이트 코드를 조작하는 코드를 작성하고, 결과를 다른 파일에 저장한다고 치자.

저장한 파일은 원본과는 다른 코드를 갖고 있다.

참고한 포스트: 자바 바이트 코드 조작하는 방법 (tistory.com)

 

바이트 코드 조작의 결과

아마도 바이트 코드를 조작해서, 이런 싱글톤 클래스를 만들었을 것이다.

  1. 사용자가 빈으로 등록한 클래스가 이미 스프링 컨테이너에 있다 → 해당 인스턴스 반환.
  2. 스프링 컨테이너에 사용자가 빈으로 등록한 클래스가 없다 → 새로운 인스턴스 생성.

 

*정리*

@Configuration은 싱글톤 패턴으로 빈을 생성하는 어노테이션이다. @Configuration이 없어도 빈은 생성된다! 다만 싱글톤 타입이 아니다.

 

@Configuration을 지우고 AppConfig.java 를 참고하여 코드를 실행한 결과는 다음과 같다:

 

생활코딩 express 강의 1강~22강을 듣고 정리한 내용입니다. 

https://youtu.be/hwknmhLKgYg

<목차>

1. Middleware(미들웨어) + Third-party middleware(제3자가 만든 미들웨어)

2. Routing(라우팅)

3. Error Handling(에러 처리하기)

4. Static contents(정적 컨텐츠 제공하기)

 

 

1. Middleware

1) 미들웨어란?

다른 사람(express 공식 개발팀 또는 제 3자)이 개발한 모듈 코드를 외부에서 가져와서 사용하는 것. 
미들웨어는 코드의 재사용성을 높여주고 코드를 간결하게 짤 수 있도록 도와준다. 

 

2) 미들웨어의 원리

const 변수 = require('미들웨어 이름');

app.use(미들웨어());

 

1. 미들웨어() 생성자를 통해서 미들웨어가 리턴된다. 
2. 리턴된 미들웨어는 app.use()를 통해서 앱에 장착된다. (= 앱에서 사용할 수 있다.)

 

(예시): helmet(웹 보안 문제의 취약점을 막아주는 미들웨어)

$ npm install helmet --save
const helmet = require('helmet');
app.use(helmet());

 

3) 미들웨어의 세분화

1) url

app.use('미들웨어 실행시킬 url', function); 

: 맨 왼쪽에는 해당 미들웨어를 실행시킬 url을 명시할 수 있다. 

app.use('/user', helmet());

: helmet() 미들웨어는 경로가 '/user'로 시작하는 경우에만 실행된다. 

('/user' 뿐만 아니라 '/user/password' 등의 '/user'을 포함한 하위 url에도 적용된다.)

2) http method

 

모든 메소드에 해당 미들웨어를 사용하고 싶다면 app.use(), 

특정 http method 요청이 들어왔을 때 해당 미들웨어를 사용하고 싶다면 메소드에 맞게 app.get(), app.post() 등을 사용하면 된다. 

 

app.use('/user', helmet());

위의 코드는 helmet() 미들웨어를 url이 '/user'로 시작하는 경우 실행하는 반면, 

아래의 코드는 url이 '/custom'으로 시작하면서 http get 방식의 요청이 들어온 경우에만 helmet() 미들웨어를 실행한다. 

app.get('/custom', helmet());

+

지금까지 라우팅에 사용하던

app.get('/url', (req, res)=>{}));

 

이 함수 역시 미들웨어의 종류 중 하나였다!

 

4) 미들웨어 연속적으로 주기

미들웨어 하나를 매개변수로 준 다음, 바로 다음에 실행될 함수를 추가 매개변수로 줄 수 있다. 
이 경우 매개변수로 주어진 순서대로 실행이 된다. 

app.get('*', (req, res, next)=>{
  console.log('first middleware')
  }),
  (req, res, next)=>{
    console.log('second middleware')
  }
});

 

5) 미들웨어 실행 순서

순서대로, 맨 위부터 실행한다. 
next()의 경우 다음에 실행될 미들웨어를 호출한다. 

만약 현재 실행되고 있는 미들웨어의 다음 매개변수로 주어진 미들웨어도 있고, 같은 url과 http method에 호출되는 미들웨어도 있는 경우:

app.get('/node/js', (req, res, next)=>{
  console.log('first middleware');
  next();		// option 1
  next('route');	// option 2
},
(req, res)=>{
  console.log('next middleware');	// option 1 result
}
);

app.get('/node/js', (req, res)=>{
  console.log('next route middleware');	// option 2 result
});

(둘 다 실행할 수 없다.)

 

 

1. next()를 실행하면 같은 미들웨어의 다음 매개변수로 주어진 미들웨어 실행
2. next('route')를 실행하면 다음 미들웨어가 실행된다.

 

+제3자가 만든 미들웨어(Third-party middleware)

사용하는 방법:

1) npm으로 패키지 설치

$ npm install cookie-parser

 

2) require를 통해 패키지 불러오기

const cookieParser = require('cookie-parser');

 

3) app.use()를 통해  해당 미들웨어를 app 인스턴스에 장착하기

app.use(cookieParser);

위 단계를 거치면, 각 라우터 미들웨어 안에다 부가적인 코드를 작성하지 않아도 미들웨어에 해당하는 http 방식과 url로 요청이 들어올 때 미들웨어가 활성화된다. 

 

[1] bodyParser

 

이 미들웨어는 클라이언트가 post방식으로 보낸 http request 중에서, body 부분을 parameter로 추출해 준다. 
그래서 post 방식에서 사용할 수 있다(사용자가 보낸 데이터가 있어야 한다)

// 필요한 코드가 생략된 sudo code
const fs = require('fs');
const bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({extended: false}));

app.get('*', (req, res, next)=>{
  fs.readdir('./data', function(err, filelist){
    req.list = filelist;
    next();
  });
});

예시 코드에서, 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(라우터)라고 한다. 

(출처: 라우팅 (Routing) : 네이버 블로그 (naver.com))

 

2) url에 parameter를 실어서 보내는 방법

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번 파일의 주소를 변수로 불러온다.

(예시)

const indexRouter = require('./routes/index');
const topicRouter = require('./routes/topic');

 

3. 2번에서 선언한 변수(indexRouter, topicRouter)를 app.use()를 사용해서 app.js 파일에서 미들웨어로 사용하겠다고 선언한다. 이때 app.use('라우터와 연결할 url', 사용할 변수 이름); 으로 입력한다. 

app.use('/', indexRouter);
app.use('/topic', topicRouter);

 

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에서는 그래야만 해당 미들웨어가 에러를 처리하는 역할을 하도록 정해 놓았다.)

// other middlewares

app.use((err, req, res, next)=>{
  // error handling
});

 

2. express에서 기본으로 제공하는 에러 처리 미들웨어 사용하기.

next() 함수의 매개변수로 err(에러)을 넣어 주면 된다. : next(err)

express에서는 별도의 처리를 하지 않아도, 해당 에러를 처리해 주는 미들웨어를 제공한다. 

router.get('/example/url', (req, res, next)=>{
  // code
  if(err){
    next(err);
  } else {
    // code
  }
});


4. 정적 파일(static file) 제공하기

express.static('정적인 파일이 있는 폴더의 경로');
: 해당 디렉토리에 해당하는 파일들은 정적인 파일들로 불러올 수 있다. 

app.use(express.static('정적인 파일이 있는 폴더의 경로'));
: 해당 디렉토리 안의 파일들을 웹 어플리케이션에서 정적 파일들로 사용할 수 있다. 

+ Recent posts