[입문] 프로젝트 환경설정/스프링 웹 개발 기초/회원 관리 예제-백엔드 개발/스프링 빈과 의존관계

너스레:)·2023년 4월 4일
0

스프링Spring

목록 보기
1/5
post-thumbnail

1. 프로젝트 환경설정

ⓐ 프로젝트 생성

gradle / maven => 필요한 라이브러리를 땡겨서 오고, 얘가 빌드하는 라이프사이클까지 다 관리해주는 툴
과거에는 maven을 주로 사용했으나 요즘에는 gradle 사용

dependencies => 어떤 라이브러리 땡겨서 쓸거냐

thymeleaf => a modern server-side Java template engine for both web and standalone environments. Allows HTML to be correctly displayed in browsers and as static prototypes.

요즘에는 maven이고 gradle이고, 모두 main, test로 폴더가 나눠져있음
test 코드라는 게 요즘 개발에서 굉장히 중요하다는 거임

gradle에 관해서는 공부하는 게 좋은데 일단은 나중에 공부하길 권장
일단 gradle => 버전 설정 & 라이브러리 땡겨옴 으로 알고 있으면 편함

깃에는 필요한 소스코드들만 올라가야지, 빌드된 결과물이 올라가면 안 됨

Tomcat이라는 웹 서버를 내장하고 있음

설정에서, gradle > 'Build and run using:', 'Run tests using:'을 gradle이 아닌 "IntelliJ IDEA"로 바꿔줄 것
gradle을 통해서 실행하면 속도 느려서

ⓑ 라이브러리 살펴보기

요즘 gradle/maven : 라이브러리의 의존관계를 알아서 관리해줌 다 땡겨와줌
라이브러리의 의존관계(dependendies)는 IDE 오른쪽 바의 Gradle 창의 Dependencies에서 확인할 수 있음

  • spring-boot-starter-web의 의존관계
    => tomcat, spring-webmvc
  • spring-boot-starter-thymeleaf의 의존관계
  • spring-boot-starter의 의존관계
    => spring-boot, logging(slf4j, logback)
  • spring-boot-starter-test의 의존관계
    => junit 라이브러리(요즘은 버전 5 多)를 대부분 사용함, mockito, assertj, spring-test

ⓒ View 환경설정

src > main > resources > static => "정적" 페이지
index.html => 웰컴 페이지 (홈 화면)

원하는 기능을 찾고 싶으면
spring.io 공식 페이지에서 검색해보면 됨
예시로, Welcome Page에 대해 알고싶다면 =>
https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.servlet.spring-mvc.welcome-page

'템플릿 엔진'을 쓰면
'정적' 페이지가 아닌, 프로그래밍이 되는 페이지를 만들 수 있음
웹 애플리케이션에서 첫 진입점이 @Controller 임
th => thymeleaf 문법

ⓓ 빌드하고 실행하기

개인적인 궁금증 - 1

Q. 빌드 전에도 웹 브라우저에 동적 html 파이라 띄울 수 있는데 왜 굳이 빌드&실행을 하는가?
=> A. https://itholic.github.io/qa-compile-build-deploy/

2. 스프링 웹 개발 기초

스프링 웹 개발 -- 3가지 방법
1) 정적 컨텐츠 => 서버에서 뭐 하는 거 없이 파일을 "그대로" 웹 브라우저에 출력
2) MVC 템플릿 엔진 => 가장 많이 하는 방식. 서버에서 html을 프로그래밍해서 동적으로 바꿔서 출력함. Model + View + Controller = MVC
3) API => 안드로이드/아이폰 클라이언트와 개발할 때, 예전에는 XML 포맷을 썼지만 요즘은 Json 데이터 구조 포맷으로 클라이언트한테 데이터 전달. 서버끼리 통신할 때도 사용.

1) 정적 컨텐츠

먼저 컨트롤러 쪽에 hello-static 관련 controller가 있나 찾아봄.
즉, 컨트롤러가 더 우선순위를 가진다는 말임
없다는 걸 확인했으면 resources: static/hello-static.html을 그대로 웹 브라우저에 반환해줌

2) MVC와 템플릿 엔진

MVC : Model, View, Controller
과거에는 view와 controller가 따로 분리되어있지 않았었음
view에서 모든 걸 다 했었음 ; Model 1 방식
현재는 MVC 스타일로 많이 함
왜냐하면 view는 화면을 그리는 데에 모든 역량을 집중해야 함
controller, model은 비즈니스 로직 관련, 서버 뒷단 관련을 처리하는 데 집중해야 함
그래서 요즘은 controller와 view를 쪼개는 게 기본임
model에다가 화면 쪽에 필요한 것들을 담아서 화면쪽에 넘겨주는 방식

thymeleaf 템플릿 엔진의 장점 : html 파일을 서버 없이 바로 열어봐도 껍데기를 볼 수 있음

hello! empty

템플릿 엔진으로서 동작을 하면 hello! empty 부분이 'hello ' + ${name} 로 치환이 되어 출력됨
그래서 굳이 hello! empty 부분을 넣어놓은 이유는, 서버 없이 그냥 html 만들어서 볼 때 html 마크업 해주는 사람들이 값을 적어놓고 볼 수 있도록 하기 위함임. 어차피 서버를 타서 돌면 치환된 값으로 보임.

http://localhost:8080/hello-mvc?name=spring!!!!!!!

viewResolver : 화면과 관련된 해결자; 뷰를 찾아주고, 템플릿 엔진 연결시켜주는 역할
정적 컨텐츠일 때는 변환 없이 '그대로' 출력을 해줬다면, MVC에서는 Thymeleaf 템플릿 엔진이 '변환을 해준 뒤에' 웹브라우저에다 넘겨줌

3) API

정적 컨텐츠를 제외하면, HTML로 내릴 것이냐(MVC), 아니면 API라는 방식으로 데이터를 바로 내릴 것이냐 만 선택하면 됨

@ResponseBody 넣어줘야 함 ; HTTP (html 아님)의 header와 body 중 BODY 부분에 이 데이터를 '직접' 넣어주겠다는 뜻
view 이런 거 없이 그냥 저 데이터가 그대로 내려감
진짜 '페이지 소스 보기' 해보면 html 태그 같은 거 없이 정말 데이터만 딱 보임.

json 방식 : 키-값으로 이뤄진 구조
과거에는 xml 방식으로 많이 썼었음. xml 방식이라고 한다면 html 태그 있는 방식 말함.
근데 xml 방식은 무겁고, 태그를 열리는 태그, 닫히는 태그 해서 2번 써야 하는데,
json은 그냥 key: value 식으로 보여주니까 심플함.
요즘에는 json 방식으로 거의 통일됨. 스프링에서도 json을 기본으로 함.

getter, setter => JavaBeans 표준 방식, property 접근 방식

@ResponseBody => viewResolver가 아닌, "HttpMessageConverter"로 전달

HttpMessageConverter에서
단순 string이면 => StringConverter 동작 (StringHttpMessageConverter)
객체라면 => JsonConverter 동작 (MappingJackson2HttpMessageConverter) => 객체를 json 스타일로 바꿔서 응답함
객체를 json으로 바꿔주는 유명한 라이브러리들 (크게 2가지) => Jackson2(실무에서 多; 스프링에서는 이걸 기본으로 채택) & Gson(구글에서 만듦)

참고로 HTTP의 Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합하면 그 타입에 맞는(예를 들어, xml) HttpMessageConverter가 선택되어 돌아감

일반적으로 'api'라고 하면 '객체'를 반환해서 json으로 변환하는 걸 말함

3. 회원 관리 예제 - 백엔드 개발

① 비즈니스 요구사항 정리

② 회원 도메인과 리포지토리 만들기

Optional => java 8에 추가된 기능 ; null을 처리할 때 null을 그대로 반환하기보다는 Optional로 감싸서 반환하는 것을 선호함

실무에서는 공유되는 변수일 경우에는 동시성 문제가 발생할 수 있어서 ConcurrnetHashMap 써야 하는데, 여기서는 예제니까 그냥 단순한 HashMap 쓰겠음
long도 동시성 문제 때문에 실무에서는 그냥 long이 아닌 AtomicLong을 대부분 사용함

③ 회원 리포지토리 테스트 케이스 작성

코드를 코드로 검증하는 방법
자바에서는 JUnit이라는 프레임워크test 코드를 만들고 실행함.

test 코드는 굳이 "public"으로 하지 않아도 됨. 다른 곳에서 갖다 쓸 게 아니니까.

test => org.junit.jupiter.api.Test

org.assertj.core.api의 Assertions => org.junit.jupiter.api의 Assertions 보다 더 편하게 쓸 수 있음

static import 해버려서 앞에 Assetions. 부분을 안 쓰고 바로 assertThat()부터 쓸 수 있도록 할 수 있음

실무에서는 test랑 빌드 툴이랑 엮어서, 빌드 할 때 테스트케이스 통과하지 않으면 다음 단계로 못 넘어가게 막아버림

테스트케이스의 장점 : run을 하나의 class 단위, 전체의 class 단위 등등 테스티 level을 달리해서 할 수 있음

따로따로 method별로 run해봤을 때는 문제 없었는데 그 function들이 들어있는 class 단위로 run해보니까 error가 발생한 경우
--> 그 이유 : test 시, class 내부의 method별 '순서'는 '보장되지 않는다'. 적힌 순서대로가 아니고 무작위로 실행이 되는데, 만약 findAll()이 실행된 후에 findByName()이 실행된다면 이미 findAll() 쪽에서 spring1, spring2를 저장해놨기 때문에 findByName에서는 findByName에서 저장한 spring1, spring2가 아닌, findAll에서 저장한 spring1, spring2가 나와버려서 error가 발생한 거임
--> 해결방법 : test 하나가 끝날 때마다 다음 test에 영향주지 않게 하기 위해서 data를 "clear"해줘야 함. MemoryMemberRepository 파일에 그 코드를 넣어주면 됨. 그리고 test 파일에서는 @AfterEach라는 애노테이션(각 method의 실행이 끝날 때마다 동작하는 callback method)
=> test는 method들 간의 순서에 의존관계를 갖지 않도록 설계되어야 함

지금은 MemoryMemberRepository를 먼저 개발한 뒤에 test를 작성했는데,
순서를 뒤집어서 test를 먼저 작성하고 MemoryMemberRepository를 작성할 수도 있음!
마치 내가 만들 작품의 '틀'을 먼저 만들어놓고 작품을 실제 만들면 그 틀에 꽂히는지 안 꽂히는지
=> TDD (테스트 주도 개발; Test-Driven Development)

test가 수십수백개여도 문제 없음
build를 하거나, 패키지 단위로 run을 하거나, gradle에서 test를 하거나 할 수 있음
test 자동으로 다 돌려줌

test 없이 개발하는 건 혼자 할 땐 어떻게 된다고 쳐도, 여러 사람들과 함께 작업할 때와 코드 수가 몇 만 라인을 넘어가게 되면 test 없이 개발하는 건 거의 불가능. 문제 많이 생김

그래서 test 관련해서는 깊게 공부 필요함

④ 회원 서비스 개발

회원 서비스 => 비즈니스 로직 有
회원 서비스 => 회원 repository와 domain을 활용해서 실제 비즈니스 로직을 작성

ifPresent() => null이 아니라 어떤 값이 있으면 이 로직이 작동함
이건 Optional이라서 가능한 것임
기존에는 if result != null 이런 식으로 했겠지만
Optional로 한 번 감싸면 Optional 안에 member가 들어있게 됨
Optional에는 여러 method 존재 -- 잘 활용할 것
요즘에는 null일 가능성이 있으면 Optional으로 한 번 감싸서 반환해줌
그냥 직접 꺼내고 싶으면 .get()으로 꺼내도 되긴 함. 근데 직접 바로 꺼내는 걸 권장하진 않음.
Optional의 method 중에 orElseGet() method도 자주 씀

Optional을 바로 반환하는 게 별로 좋지 않음
권장하는 건, 바로 ifPresent 붙여버리는 것

로직이 나오면 method로 따로 뽑는 게 좋음

repository는 save(), findById(), findByName(), findAll()처럼, 단순히 저장소에 넣었다 뺐다 하는 느낌 有 ; 개발적
하지만 service는 join(), findMembers()처럼, 뭔가 더 '비즈니스' 관련한 기능들 ; 비즈니스 의존적

⑤ 회원 서비스 테스트

자동으로 생성하면 Assertions import가 알아서 되어있음
근데 이때 Assertions가 'junit' 껄로 되어있을 거임. 그래서 assertThat()을 못 쓰는데
junit 말고 "assertj" 껄로

test는 사실 과감하게 '한글'로 바꿔도 됨
빌드될 때 어차피 test 코드는 실제 코드에 포함되지 않음

//given
//when
//then

식으로 적는 것을 적극 추천함
test는 사실 생각해보면
뭐가 주어졌는데
뭐를 실행했을 때
그러면 결과가 이걸로 나와야 한다
test 코드가 간단하면 몰라도 test 코드가 막 길어지면 뭐가 뭐를 위한 코드인지 구분이 잘 안 가는데
이렇게 given, when, then으로 주석을 달아 작성하면 구분도 잘 되고 코드 짜기 좋음
상황에 따라 이 given, when, then이 안 맞을 때도 있는데 그때는 알아서 변형해서 써가면 좋을 것

테스트는 정상 플로우도 중요하긴 하지만 '예외 플로우'가 훨씬 더 중요함
join()의 핵심은 저장이 되는 것도 중요하지만, 중복 회원 검증 로직에서 예외가 터뜨려지는 것도 확인해봐야 함

try catch 문으로 예외를 처리해도 되지만 더 좋은 문법은 assertThrow로 처리하는 것임
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
2번째 파라미터 로직을 실행하면 첫번째 파라미터 예외가 터져야함
만약 '다른' 예외가 터졌다면? => 테스트 실패
assertThrow는 에러 메세지를 반환함

여기서도 @AfterEach로 clear를 해줘야 함
근데 문제 有 : MemberService밖에 없음
=> 따라서 MemoryMemberRepository를 들여와야 함

MemberService와 MemberServiceTest에서 new로 MemoryMemberRepository() 객체를 따로따로 만들어놔있음
-- 근데 '다른' 2개를 사용할 필요가 있나?
'같은' 걸로 사용하는 게 좋음
=> memberRepository를 내가 직접 new로 생성해주는 게 아니라, '외부에서' 넣어주도록 바꿔줌 -- constructor
@BeforeEach => 동작하기 '전'마다
이게 바로 "DI(의존관계 주입 ; Dependency Injection)"

[오류] 회원가입() : "No tests were found"

=> 해결 : https://junho85.pe.kr/1527
=> 김영한님 official : https://www.inflearn.com/questions/165635/test%EC%97%90%EC%84%9C-no-tests-were-found-%EC%98%A4%EB%A5%98%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%95%A9%EB%8B%88%EB%8B%A4

# 정리

지금까지 멤버 서비스 & 멤버 레포지토리 & 멤버 객체 만들었음.
서비스를 통해 멤버가 가입하면 레포지토리에 저장이 되고 꺼내올 수도 있었음.
그리고 테스트도 만듦.

4. 스프링 빈과 의존관계

화면을 붙이고 싶다면, 컨트롤러 & 뷰 템플릿이 필요할 것.
멤버 컨트롤러는 "멤버 서비스를 통해" 회원가입하고 멤버 조회를 할 수 있어야 함
=> '멤버 컨트롤러가 멤버 서비스를 의존한다' => '의존관계 有'

1) 컴포넌트 스캔 & 자동 의존관계 설정

@Controller
=> 스프링이 처음에 뜰 때 '스프링 컨테이너'라는 통이 생기는데, 이 애노테이션이 있으면 컨트롤러를 '객체'로 생성해서 spring에 넣어두고 spring이 이를 관리함
=> '스프링 컨테이너에서 스프링 빈이 관리된다'

cf) MVC & 템플릿 엔진

private  final MemberService memberService = new MemberService();

이렇게 "new" 키워드로 새로 생성해서 쓸 수 있을 거임.
BUT spring이 관리하게 되면 다 스프링 컨테이너에다 등록을 하고, 스프링 컨테이너로부터 받아서 쓰도록 바꿔줘야 함!
왜냐하면
new 키워드로 생성할 시의 문제점
: MemberController 말고도 다른 여러 Controller들이 MemberServie를 가져다 쓸 수 있을 것. 근데 MemberService는 별 기능이 없어서 여러 개 생성할 필요가 없음. 그냥 "하나만" 생성해놓고 같이 공유해서 쓰면 됨.

컨테이너에 등록해서 '하나'로 사용 => "싱글톤"
스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 유일하게 하나만 등록해서 공유함
따라서 같은 스프링 빈이면 모두 같은 인스턴스다.
물론 싱글톤이 아니도록 설정이 가능하긴 하지만 , 특별한 경우가 아니라면 대부분 싱글톤을 사용한다.
웬만한 건 스프링 빈으로 등록해서 쓰는 게 좋음
(그렇게 해야 얻는 이점이 많은데, 왜 그런지는 나중에 설명 예정)

'애노테이션' 을 달아줘야 스프링이 뭔지 파악을 해서 컨테이너 내에 생성을 해서 연결해줄 수 있음
@Service(비즈니스 로직), @Repository(데이터 저장), @Controller(외부요청 받음)
정형화된 패턴임 ; controller, repository, service
애노테이션을 안 달아주면 그저 순수한 JAVA 클래스라서 스프링이 이게 뭔지 파악할 방법이 없음
@Autowired(엮어줌; DI)
연결까지 잘 해주려면, 스프링 컨테이너가 뜰 때 멤버컨트롤러를 생성해주면서 멤버컨트롤러의 생성자가 호출되는데, 그때 생성자에 @Autowired 애노테이션이 붙어있으면 스프링이 스프링 컨테이너에 있는 멤버서비스를 가져다 딱 연결을 시켜주면서 스프링 컨테이너에 다같이 넣어줌

@Service, @Repository, @Controller
=> 공통점 : 모두 다 Component를 내포하고 있음

component_service component_repository component_controller

=> "컴포넌트 스캔"

해당 패키지 "이하의(동일+하위)" 패키지들을 컨트롤러 스캔을 통해 component로 자동 등록함.
즉, 해당 패키지 이하의 패키지들이 아닌 것들은 스프링 빈으로 컴포넌트 스캔 안 함.

2) 자바 코드로 직접 스프링 빈 등록하기

하나하나 스프링 설정 파일(SpringConfig 파일)에 직접 등록하는 방법

애노테이션들 다 지울 것 (Controller 빼고)
Controller는 어쩔 수 없이 Spring이 관리해야 하기 때문에 '컴포넌트 스캔('1)' 방법)으로 @Autowired 설정해서 멤버서비스를 땡겨와야 함

@Configuration, @Bean

MemberController SpringConfig

# 정리

1) DI의 3가지 방법

01) 생성자 주입

  • 권장
  • 처음 조립 시점에 생성자로 딱 한 번만 조립해놓고 끝을 내버릴 수 있음. 건들지 않아도 될 메소드를 절대 건들지 못하게 막아버릴 수 있음.

02) setter 주입

  • 생성과 set를 분리할 수 있음
  • 단점 : 한 번 setting이 되고 나면 바꿀 일이 거의 없는데, setter 주입으로 하게 되면 set 메서드가 public하게 노출되어 아무 개발자나 호출해서 아무렇게나 변경 가능하게 됨

03) 필드 주입

  • 좋은 방법이 아님
  • 중간에 바꿔치기 할 수 없다

2) 선택 기준

  • 정형화 O => '1번 방법: 컴포넌트 스캔 & 자동 의존관계 설정' 권장
  • 정형화 X => '2번 방법: 자바 코드로 직접 스프링 빈 등록하기' 권장
    • 나중에 해볼 예정(MemoryMemberRepository 구현체 → DbMemberRepository)
    • 상황에 따라 구현 클래스를 변경해야 할 때, 딱 설정 파일만 바꿔주면 다른 파일들에 손 댈 필요없이 바꿔치기 가능

3) 주의

@Autowired를 통한 DI => 스프링이 관리하는 객체에서만 동작함.
즉,
스프링 빈으로 등록된 게 없음, 또는, 내가 new로 직접 생성한 객체 => @Autowired 안 먹힘

유용한 단축키

  • shift + f6 => 이름 다시 짓는(rename) 단축키
  • Ctrl + Alt + V => 파라미터 추출하기 (김영한님이 제일 좋아하는 단축키)
  • Ctrl + Shift + Alt + T => 리팩토링 단축키
  • shift + F10 => 이전에 실행했던 거 다시 실행해주는 단축키
  • Shift + Ctrl + T => test 코드 자동 생성 단축키
profile
💻 (CSE) Computer Science and Engineering

0개의 댓글