스프링 입문 - 1

개발자가 되고 싶어요 ㅣ 2024. 1. 20. 03:05

스프링 입문 - 1

메이븐과 그레들의 차이

메이븐과 그레들은 필요한 라이브러리를 땡겨주거나, 얘가 빌드하는 라이프 사이클까지 다 관리해주는 툴이다.

과거에는 메이븐, 요즘에는 그래들을 쓴다.

레거시 프로젝트나 과거 프로젝트는 메이븐으로 남아있는게 많은데 요즘 추세는 그레들로 넘어가는 추세다

스프링 라이브러리 관리 자체도 요즘에는 그레들로 넘어온 추세다.

프로젝트 이름

group은 보통 기업 명을 선호한다 우리는 상관 없으니 hello 라고 하겠다

artifact는 빌드 나올 때 결과물을 얘기한다고 보면 된다 우리는 hello-spring이라고 하겠다. 결국 프로젝트 명을 지정한다고 보면된다.

dependencies

우리는 spring web을 만들 거기 때문에 spring web을 추가해준다.

그리고 html을 만들어주는 템플릿 엔진이라는게 필요한데 thymeleaf라는걸 사용할 것이다.

스프링부트 파일 구조

.idea: 인텔리제이가 사용하는 어떤 설정 파일

gradle: 그레들과 관련되서 그레들이 쓰는 폴더

src: maven과gradle 모두 요즘에는 기본적으로 main과 test로 구분되어 있는데, main에는 앱 패키지와 resources가 있고 테스트에는 앱패키지가 있다.

resources에는 실제 java코드 파일을 제외한 어떤 xml이나 프로퍼티스나 html등과 같은 어떤 설정파일이 들어가 있다.

build.gradle: 옛날에는(그냥 spring) 일일이 작성해야했지만 요즘에는 스프링부트가 나오면서 파일 생성시에 위 depencies에서처럼 추가해주면 알아서 작성해준다. 쉽게 버전설정과 라이브러리를 땡겨와준다고 생각하면 된다.

안에 내용을 보면 우리가 추가한 thymeleaf와 spring web이 들어가있다.

추가로 테스트코드에 대한 부분이 기본적으로 들어가는데 JUnit 5라는 라이브러리다.

이 라이브러리들이 어디서 자동으로 다운받아지냐 궁금할 수 있는데

repositories{
	mavenCentral()
}

라는 부분이 있는데 mavenCentral()이라고 설정된 url에서 다운받아진다고 생각하면된다.

기본 실행 메커니즘

메인메서드를 실행하면 SpringApplication.run이 실행되는데, apllicaiton클래스는 기본적으로 @SpringBootApplication이 달려있다. 그러면 스프링이 내장하고 있는 tomcat이라는 웹 서버를 자체적으로 띄우면서 스프링부트가 같이 띄워지게 된다.

라이브러리에 대해

기본적으로 maven이나 gradle같은 경우는 의존관계를 다 관리해준다.

이게 무슨말이냐면 내가 springbootstarterweb 라이브러리를 땡기면 springbootstarterweb이 의존하고 있는 다른 라이브러리들까지 다 땡겨와준다는 거다. 또한 중복 라이브러리는 *처리로 구분지어 중복을 제거해준다.

톰캣에 관해서는 예전에는 톰캣을 직접 설치해서 설정을 해줬어야 했지만 요즘에는 그냥 소스 라이브러리에서 이런 웹서버를 들고 있다. 이를 임베디드 내장하고 있다고 표현한다. 실행만 해도 웹서버가 뜨고 8080포트로 들어가지는 걸 보면 알 수 있다.

Log에 관해서

실무에서는 system.out.prinln 과 같은 출력을 하면 안된다. 나중에 모아서 보거나 관리하기 위해서는 log로 출력해야한다. 그렇기 위해서 요즘에는 logback과 slf4j라는 라이브러리를 조합해서 사용하는 경우가 많다.

총 정리

스프링부트 라이브러리

  • Spring-boot-starter-web
    • Spring-boot-starter-tomcat: 톰켓 (웹서버)
    • Spring-webmvc: 스프링 웹 MVC
  • Spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
  • Spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
    • Spring-boot
      • Spring-core
    • Spring-boot-starter-logging
      • Logback, slf4j

테스트 라이브러리

  • Spring-boot-starter-test
    • Junit: 테스트 프레임워크
    • Mockito: 목 라이브러리
    • Assert: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
    • Spring-test: 스프링 통합 테스트 지원

Welcome 페이지

Index.html로 파일명을 지정하면 기본적으로 아무 uri를 작성하지 않아도 localhost:8080을 url에 입력했을 때 접속 가능하다. 자세한 내용은 docs.spring.io 페이지에 있다.

타임리프

템플릿 엔진으로써 html 파일에 내가 원하는 루프를 넣어주거나해서 모양을 바꿔줄 수 있다.

우리는 일단 타임리프라는 템플릿 엔진을 사용할거다.

Controller 패키지를 생성하고 controller 자바파일을 만든다.

@Controller라고 어노테이션을 지정해주면 자바에서 컴파일하고 url 매핑할 때 컨트롤러를 거쳐서 url을 매핑해준다.

@GetMapping(“uri”) 해주면 웹에서 “uri”로 연결되었을 때 실행되는 함수로 선언할 수 있다.

Model 매개변수를 넣으면 model에 addAttribute(“data”,”값”) 값을 넣어 data 라는 변수로 웹에 보내줄 수 있다.

웹에서는 <html> 태그에 <html xmlns:th=”http://www.thymeleaf.org">를 선언해주면 템플릿엔진이 작동하게 되는데 예시로 <p> 태그에 <p th:text=”’안녕하세요’+${data}”></p>

처럼 작성해주게 되면 th가 달려있는 값에 아까 controller에서 받은 model값을 넣어 줄 수 있다.

값을 받을 때엔 ${변수명}을 해주면 된다.

그리고 나서 서버를 작동하고 웹에 들어가보면 model에 넣어줬던 값이 잘 표현되는걸 확인할수있다.

controller에서 적었던 return값으로 문자를 반환하면 뷰 리졸버가 리소스 파일의 템플릿 파일(‘resources:templates/’ +{return값} +’.html’)을 뒤져서 해당 리턴값 파일을 찾은 다음에 렌더링해준다.

그럼 html파일을 수정할 때마다 서버를 껐다 켰다해야하냐는 물음이 있을 수 있는데

Spring-boot-devtools 라는 라이브러리를 추가하면 , 첫 작동시 컴파일 된 html 한에서 서버 재시작 없이 view파일 수정이 가능하다.

cmd에서 프로젝트로 들어가서 gradlew.bat을 build 해주고(gradlew.bat build) libs 파일에 들어가보면 snapshot이 포함된 파일명의 jar파일이 생성되어있는데 이거를 나중에 배포할 때 쓴다고 한다. 추가적으로 cmd에서 해당 jar파일을 실행하면 ide같은 툴에서 실행 안 해도 서버가 열린다. 하지만 같은 8080port를 두 군데에서 사용할 수 없으므로 하나의 실행만 해야한다.

스프링 웹 개발 기초

  • 정적 컨텐츠
    • 서버에서 뭐하는 거 없이 웰컴 페이지처럼 ‘파일 그대로’를 렌더링하는 방식
  • Mvc와 템플릿 엔진
    • 가장 많이 쓰는 방식으로 jsp,php와 같은 템플릿 엔진으로 서버에서 프로그래밍해서 뭔가 변형된 내용을 html에 뿌려주는 방식
  • Api
    • 안드로이드나 아이폰 클라이언트와 개발할 때 서버입장에서 json같은 데이터 구조 포맷으로 값을 클라이언트에게 전달해주는 방식
    • 요즘엔 보통 vue나 react, vue.js 등을 쓸 때 api로 데이터만 내려주면 클라이언트가 알아서 화면에 그리거나, 서버끼리 통신할 때와 같이 데이터가 흐를 때 쓴다고 할 수 있다.

정적 컨텐츠

정적 컨텐츠는 resources에 html 파일을 생성하고 uri에 파일이름과 파일형식(localhost:8080/hello-static.html)까지 입력해주면 그대로 열리게 된다.

작동원리는 uri에 hello-static이 들어오면 컨트롤러가 우선순위를 가지고 hello-static 관련 컨트롤러를 찾아본다. 없다면 resources 파일을 뒤져서 반환해준다.

추가로 인덱스 파일과 컨트롤러 둘 중 누가 더 우선순위를 가질까?가 궁금해서 실험해봤더니 컨트롤러가 우선순위를 가지더라

MVC방식

MVC와 템플릿 엔진 중 MVC는 model view controller로 개발을 나눈 거다.

예전에는 보통 view에서 jsp를 활용하여 모든 개발을 했다. 이를 모델원 방식이라고 한다.

개발을 배우다 보면 관심사를 분리해야한다, 역할과 책임 같은 말을 많이 듣는데 mvc로 나눈이유는 view는 화면을 그리는데에 집중하고 controller는 비즈니스 로직이나 내부적인걸 처리하는데에 집중을 해야하기 때문이다. Model은 화면에 필요한 데이터를 뷰에 넘겨주는 역할을 한다. MVC의  장점으로는 역할을 나누면 유지보수가 편해진다는 점이 있다.

매개변수에 @RequestParam(“이름”)선언해준 변수명이 있다면 url에 입력하고 뒤에 ?이름=값 을 입력해서 해당 변수명으로 model로 넘겨줄 수 있게 된다. 근데 사실 아직까지는 @RequestParam을 언제 활용하는지는 감이 안잡힌다. url뒤에 값이 들어가야 하는데 클라이언트 입장이나 서버입장 둘 다 개발하는데 있어서 불편하지 않은가? 이건 나중에 개발하다 보면 알게 되겠지….?

html파일에서 th를 작성하고 model의 값을 꺼내올 때 ${}의 형식으로 받아온다.

정적 컨텐츠와 mvc방식의 차이를 정리해보자면 일단 url이 톰켓 서버로 들어와서 스프링 컨테이너에 들어오면 해당하는 컨트롤러가 로직을 처리하고 리턴한 값을 뷰 리졸버에 넘겨준다. 그럼 뷰리졸버가 리턴한 값으로 해당 html파일을 찾아서 변환한 후에 웹 브라우저에 뿌려준다.

여기서 정적 컨텐츠는 변환하는 과정이 없다 왜냐하면 그 파일 그대로 넘겨줄 뿐이기 때문이다. 대신 mvc방식은 뭔가 값이 바뀌고 할당되어 렌더링 되야 하기 때문에 그 과정을 변환하여 넘겨준다고 표현한다.

API 방식

뷰를 찾아가지고 템플릿 엔진을 통해서 어떤 화면을 렌더링해서 html을 웹 브라우저에 넘겨주는방법이 있고 api를 쓰는 방식이 있다. 그 둘의 차이는 결과적으로 html을 내려주느냐 데이터를 내려주느냐의 차이라고 보면 된다.

일단 @GetMapping을 넣어준 아래에 @ResponseBody를 넣어줬다. 이는 http의 헤더와 바디부분에서 바디부에 리턴값을 직접 넣어서 response하겠다는 의미이다.

이번에는 내부에 Hello라는 객체를 생성하고 진행하려한다.

Static class로 선언하면 상위클래스 내라면 어디서든 해당 클래스를 사용 가능하다. Hello 안에private String name을 생성해서 게터세터를 넣어준 다음 진행했다. 참고로 private은 외부에서 바로 못꺼내게끔 하는 선언이다. 그래서 게터세터가 필요하다.

마찬가지로 @ResponseBody를 넣어주고, 매개변수에는 @RequestParam을 넣어줘서 url에서 값을 받아올 것이다. 로직안에는 Hello 객체를 만들고 hello.setName(name)으로 url을 통해 넘어온 파라미터 값을 hello의 name에 세팅해준 다음 name을 반환해준다. 이때 처음으로 여태처럼 스트링스 반환해준 것이 아닌 객체를 반환해준거다. 이게 바로 api방식이다. 웹에 접속해보면 name값이 json 방식으로 웹에 뿌려진다.

@ResponseBody에 대해 설명하자면 웹 브라우저에서 url이 들어온다 그럼 내장 톰켓 서버에서 스프링컨테이너에 던져준다. 그럼 해당 컨트롤러를 찾아서 로직이 실행되는데 이때 @ResponseBody가 있다면 원래는 뷰리졸버로 가야할 리턴값이 ‘HttpMessageConverter’ 라는 곳으로 가게 된다. HttpMessageConvet는 StringConverter, JsonConverter등 으로 나뉜다.만약 리턴값이 스트링이라면 StringConverter로 넘어가서 그대로 웹에 반환해준다. 근데 이때 넘겨준 리턴값이 객체다? 이러면 JsonCoinverter로 넘어가서 객체를 json 데이터 구조로 변환해서 넘겨주게 된다.

이때 Jackson과 Gson이라는 라이브러리가 있는데 이는 객체를 Json으로 바꿔주는 라이브러리들이다. 스프링은 기본적으로 Jackson을 이용한다. 실무에서 많이 보게 된다고 하니 기억해두자.

앞으로 만들어 볼 비즈니스의 요구사항 정리

데이터: 회원ID, 이름

기능: 회원 등록, 조회

아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

일반적인 웹 애플리케이션 계층 구조

컨트롤러 – 서비스 – 리포지토리 – db

도메인은 컨트롤러, 서비스, 리포지토리 와 연결되어 있다.

컨트롤러: 웹 MVC의 컨트롤러 역할

서비스: 핵심 비즈니스 로직 구현

리포지토리: 데이터베이스에 접근, 도메인 객체를 데이터베이스에 저장하고 관리

도메인: 비즈니스 도메인 객체, 예)회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

클래스 의존관계

멤버서비스가 있고 멤버리포지토리는 인터페이스로 놓고, 구현체로 메모리멤버리포지토리를 만들 예정 (시나리오 상 디비가 선정이 안됐기 때문에 mybatis,jpa,jdbc 등등 무슨 기술을 쓸지 모르기 때문에 선정되고 나서 구현 클래스를 변경할 수 있도록 설계하기 위해서)

이게 참 이해가 안됐던 내용이 었는데 지금 보니까 이해가 되는 것도 같다.

인터페이스로 구현하면 일단 껍데기더라도 서비스 로직을 작성하는데에 문제가 없다. 만약 디비가 선정되고 기술이 채택되면 그때 가서 구현체를 다시 작성하면 그만이니까.

만약 인터페이스 없이 그대로 구현해놨다면 다시 작성할 때 너무 어지러울 것 같다.

domain에서 만든 Optional<Member> findById(Long id) 중 Optional은 내가 알기로 자료구조 중 최상위에 있는 자료구조형태이다. 근데 김영한님은 이를 null을 처리할 때 null을 그대로 반환하기보다는 opitonal로 감싸준다고 표현했다. Optional.ofNullable()로 객체를 반환하게 되면 뷰에서 뭔가로 처리할 수 있다고 한다.

일단 디비의 대체로 Map<Long, Member> store = new HashMap<>();을 만들어 주고 store에 값들을 넣어줄 거다. findByName이라는 함수에는 람다가 포함되어 있는데 먼저 함수를 적자면 store.values().stream().filter(member->member.getName().equals(name)).findAny(); 이러하다.

하나하나 해석해보자면 store이라는 hashmap에 있는 벨류 값 들 중 필터라는 함수를 사용해서 조건에 맞는 값들만 걸러줄거다. 이때 람다식 처음에 있는 member가 임의로 지정할 수 있는지는 뒤에서가서 실험해볼거다. 그리고 member에서 가져온 name값이 매개변수의 name과 일치한 것들을 모두 꺼내서 findAny 해준다. 이는 찾은것들을 반환하는 함수다. 이때 findByName함수는 리턴값이 optional로 되어있는데 만약 찾는 값이 없어서 null로 반환된다면 위에서 말한바와 같이 뷰에서 뭔가로 처리할 것이다.

이제 구현한 리포지토리를 테스트 케이스를 작성해서 검증할 것이다.

보통 구현한 기능이 정상작동하는지 확인하기 위해서는 메인메서드를 통해서 실행해보거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 하지만 이러한 방법은 준비하고 실행하는데 오래걸리고, 반복 실행하기 어렵고 여러테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

@Test를 붙이고 메서드를 작성하면 테스트메서드가 작동된다. 작동 결과를 system.out.prinln으로 하나하나 쳐 볼 수 있지만 Assertions 클래스를 임포트 해와서 사용하면 좋다. 클래스 단위의 테스트도 가능한데, 클래스의 모든 테스트가 실행되게 된다. 근데 순서가 보장이 안되기 때문에 랜덤이다. 그렇게 되면 테스트가 꼬일 수 있는 가능성이 있다. 이때는 @AfterEach라는 애노테이션을 사용한 메서드를 만들어준다. 이는 하나의 테스트가 끝나면 해당 메서드를 자동으로 호출하게 끔 만들어주는 애노테이션이다. @AfterEach를 붙인 메서드에는 테스트가 끝나면 store을 비울 수 있는 함수를 repositorty에서 만들어서 호출하게 해준다. 그러면 모든 테스트를 한번에 작동시켜도 문제 없이 검증을 할 수 있다.

우리는 지금 이미 구현해놓고 테스트를 해본 것이다. 하지만 이와 반대로 테스트를 먼저 구현해놓고 실제 구현클래스를 작성하는 방식이 있다. 이를 테스트 주도 개발(TDD)이라고 한다. 기술면접에서 몇 번 들었던 내용이고 아주 중요한 내용이다. 꼭 기억하도록 하자

이제 회원 서비스를 작성 할 건데 회원 서비스는 회원 리포지토리랑 도메인을 활용해서 실제 비즈니스 로직을 작성하는것이다.

이때 Optional을 활용하는 이유가 나온다. 만약 Optional로 안 감싸진 member의 이름 중복을 검증한다고 하자. 그럼 우리는 받은 member.getName()과 store.values().stream().filter(member->member.getName().equals(member.getName()))과 같이 꺼내서 결과가 if null이라면 통과 null이 아니라면 중복이다 라는 형식으로 작성하여 검증했을 것이다. 하지만 Optional에 포함된 다양한 메서드를 활용해서

member.getName()을 ifPresent(m->{throws new IllegalStateException(“이미 존재하는 회원입니다.”);

와 같이 한번에 해결할 수 있을 것이다.

근데 객체 생성시에는 Optional로 반환받는건 안이뻐서..? 실제로 작성할 땐 해당 값에 optional을 꺼내서 반환 받는 형식을 사용한다고 한다….

추가로 충격적인 인텔리제이의 단축키를 알려주셨다. 실제 서비스 클래스에서 Ctrl+shift+t 를 누르면 test파일에 똑 같은 패키지를 만들어 똑 같은 클래스명+Test 로 클래스를 만들어주고 실제 서비스 클래스의 모든 메서드를 @Test 애노테이션을 달아서 만들어준다. 미쳐따…

Assertions의 메서드 중 assertThrows(예외클래스, 예외가 발생해야 하는 코드)를 넣어주면 예외가 발생해야 하는 코드에서 예외가 터졌을 때 해당 예외 클래스가 매개변수의 예외클래스와 같으면 성공하고 다르거나 예외가 발생하지 않으면 실패하는 엄청난 메서드가 있다. 또한 이 메서드는 예외를 반환할 수 도 있어서 .getMessage()를 통해 메세지 검증도 가능하다.

강의 중 알게 된 새로운 사실 static으로 선언한 인스턴스는 클래스와 같은 static 영역에 저장된다. 일반적으로 프로그램 실행 시 class는 static 영역에 생성되고, new로 생성한 인스턴스는 heap 영역에 생성된다. 그리고 가비지컬렉터는 heap영역에 관여한다. 한마디로 실행되면 프로그램 종료 시까지 static 메모리 영역에 할당된 채로 존재한다. 이를 이용해서 우리는 임시db로 만든 store을 static으로 지정한것이어따….

여기서 중요한 내용이 나오는데, 강의 중 테스트 검증 시 매번 새로운 리포지토리 인스턴스를 만들어 생성하면 뭔가 하나의 리포지토리를 검증하는데 애매하다고 말씀하신다. 사실 뭐가 애매한건지는 약간 추상적으로 밖에 받아들여지지 않는다… 아무튼 생각해보면 서비스 클래스는 리포지토리에 의존해야한다. 리포지토리에 구현한 함수를 바탕으로 서비스 클래스를 작성하기 때문이다. 그렇기 때문에 먼저 서비스 클래스에 리포지토리를 final로 선언해주고 서비스 클래스가 실행될 때 자동으로 실행되는 서비스의 생성자에 리포지토리를 매개변수로 넣어주고 사용하면 어떨까? 그럼 하나의 리포지토리로 서비스를 구현할 수 있지 않을까? 아직 사실 완벽하게 와닿고 이해가 되지는 않는다. 하지만 어렴풋이 무슨 얘기를 하는 건지는 알 것 같다. 이게 바로 DI(Dependency Injection)이라고 한다. 다음 내용에서 더 자세히 알려준다고 하시니 이쯤하고 넘어가야겠다.

앞으로의 내용에서 기억해야 할 것은 전에도 나온 내용이지만 결국 컨트롤러는 서비스를 의존하고 서비스는 리포지토리를 의존한다.

MemberController를 만들었다. 여기에 @Controller를 넣어준다. 이게 무슨 역할을 하냐면 맨 처음 스프링을 실행 시키면 스프링 컨테이너라는 스프링 통이 하나 생성된다. 거기에 이 애노테이션(@Controller)이 있으면 이 컨트롤러 클래스의 객체를 생성해서 스프링에 넣어주고 스프링이 이것을 관리해준다. 이를 스프링 빈이 생성되었다고 표현한다.

컨트롤러에서 서비스의 객체를 생성 시킨다. 하지만 이번에도 MemberSerivce memberSerivce= new MemberService(); 와 같은 서비스 인스턴스를 만드는 행위는 하지 않을 것이다. 컨트롤러에서 굳이 여러 개의 서비스 인스턴스를 생성해서 사용할 필요가 없기 때문이다. 그럼 어떻게 하느냐 하면 일단 private final MemberService memberSerivce을 생성해주고 외부에서 객체를 주입시켜주는 방식으로 MemberController(memberService)  생성자를 만들어준다. 그리고 @Autowired 애노테이션을 달아준다. 이게 무슨역할을 하는지 지금부터 설명하겠다. 앞에서 설명한 바와 같이 처음 스프링을 실행 시에 @Controller 라는 애노테이션을 보면 스프링은 스프링 컨테이너에 컨트롤러 객체를 생성하여 스프링 빈으로 등록시켜주는 과정을 거친다.이때 @Autowired라는 애노테이션을 발견하면 이미 스프링 빈에 등록되어 있는 객체를 끌고와서 의존성을 주입시켜준다. 한마디로 MemberController(memberSerivce)를 예로 설명하자면 MemberController라는 객체가 생성 되면서 스프링 빈에 등록 될 때 매개변수인  memberService를 스프링 컨테이너에서 찾아서 넣어준다는 의미이다. 그럼 이때 memberService를 등록시켜준적이 없는데 어떻게 찾아서 넣어주지? 라는 의문이 들 수 있다. 이게 바로 @Service의 역할인 것이다. @Service 애노테이션을 서비스클래스에 달아줌으로 써 @Controller와 같은 스프링 빈으로써의 등록 과정을 거치게 되고 각 의존성을 자동으로 부여받게 된다. 같은 예로 @Repositroy도 있다.

이러한 과정을 Dependency Injection(DI)이라고 한다. 여태 이해가 안 되었던 스프링의 DI기능 제공이라는 말이 이해가 되는 순간이다. 스프링은 의존관계를 주입시켜준다.

자 그럼 여기서 의문이 든다. 저 애노테이션이 없다면 스프링 빈에 등록할 수 있는 방법은 없는걸 까? 궁금증이 들자마자 스프링 빈을 등록하는 2가지 방법을 김영한선생님께서 알려주신다.

첫번째, 컴포넌트 스캔과 자동 의존관계 설정

컴포넌트 스캔 방식이 우리가 방금 사용했던 @Controller, @Service, @Repository 등 애노테이션을 사용해서 스프링 빈에 등록하고 자동으로 의존관계를 주입받는 방식이다. 사실은 저 애노테이션 말고 @Component 라는 애노테이션을 사용하면 스프링 빈에 등록이 가능하다. 근데 어떻게 저 3가지 애노테이션으로 스프링 빈에 등록했냐 한다면 저 세가지 애노테이션안에는 @Component 애노테이션이 포함되어있다. 각각의 역할에 특수화되어 구현된 애노테이션일 뿐이다.

참고로 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다. 싱글톤이 뭐냐면 딱 하나만 등록한다는 것이다. 예를들어 MemberRepository에 @Repositroy를 등록하면 스프링은 여러 개의 MemberRepositroy를 스프링 컨테이너에 등록하는 것이 아니라 딱 하나의 MemberRepositroy만 등록한다는 것이다. 따라서 같은 스프링 빈이면 모두 같은 인스턴스이다. 설정으로 싱글톤이 아니게 설정할 수 있다지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다고 한다. 이는 메모리 절약과 같은 이점을 가져온다.

두번째, 자바 코드로 직접 스프링 빈 등록하기

이걸 알아보기 위해 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행한다.

우선 SpringConfig라는 자바 파일을 생성해준다. 그리고 @Configuration이라는 애노테이션을 적어준다. 이 애노테이션은 스프링에 관한 설정파일을 만들기 위한 애노테이션 또는 스프링 빈을 등록하기 위한 애노테이션이라고 이해하면 된다. 그런 다음 각 서비스와 리포지토리 메서드를 생성해준다.

Public MemberRepositroy memberRepositroy(){

	Return new memberRepository();

}

이때 @Bean 애노테이션을 넣어주면 스프링 실행 시에 @Configuration 애노테이션을 보고 스프링이 해당 파일에 들어와서 @Bean이 달려있는 메서드의  반환값을 스프링 빈에 등록해준다.

근데 이때 @Controller는 왜 냅두냐 하면 이건 내가 따로 빈에 등록할 필요도 없고 이때는 그냥 @Autowired로 스프링 빈으로 등록된 Service를 받아오면 된다.

그럼 이제 이 두가지 방법의 장단점을 확인해야겠죠?

일단 과거에는 xml이라는 문서로 이 과정을 거쳤다. 하지만 요즘에는 실무에서 xml파일로 설정하지 않는다. 자바코드로 설정을 많이한다.

그 다음에 DI에는 필드 주입, 세터 주입, 생성자 주입 3가지 방법이 있다.

우리가 한 방식은 생성자를 통해서 들어오기 때문에 생성자 주입이라고 한다.

예를 들어 만약 컨트롤러 클래스에서 서비스를 매개변수로 받아서 생성자를 만드는 생성자주입이 아니고 서비스 인스턴스를 생성할 때 @Autrowired를 선언해주면 이건 필드 주입이라고 한다.

@Autowired private MemberService memberService;

코드가 매우 간결해졌다.

하지만 이 방식은 스프링에서 권장하지 않는 방식이다. 왜냐하면 이 방식으로 의존성을 주입시키게 되면 중간에서 내가 뭔가를 바꿀 방법이 없다는 것이다. 사실 이 말은 무슨 소리인지 와 닿지는 않는다. 검색을 통해 알아봐야겠다.

그 이유는 일단 final 제어자를 사용하지 못한다. 이는 중간에 service가 다른 객체로 변경될 수 도 있다는 것을 의미한다. 이처럼 불변성을 해치기 때문에 권장하지 않는다.

또한 순환 의존성 관련으로 문제가 생길 수도 있다.

순환 의존이란 서로 다른 클래스가 서로를 참조하게 되는 상황이다. Class A가 Class B의 객체를 이용하고 Class B가 Class A의 객체를 이용하게 되면, 서로의 객체가 생성되지 않아 콜스택이 쌓이다가 결국 오버플로우를 발생시킨다. 이는 다른 의존성 주입을 통해서도 발생할 수 있지만 필드주입은 의존하고 있는 객체가 생성되지 않아도, 의존받는 객체가 생성될 수 있어 컴파일 단계에서는 문제가 없다. 결국 런타임 단계까지 가서 순환 참조를 잡아낼 수 있기 때문에 곤란해진다.

지금 이해되는 이유는 이 세가지 뿐이다. 일단 결국은 필드주입은 권장되지 않는다. 테스트 작성시 스프링의 도움을 받지 않으면 객체를 생성하기 어렵기 때문에 테스트를 돌릴 때 전체 프로젝트를 돌려야한다. 이는 간단한 코드로 테스트를 돌려보는 의미가 무색해지게 만든다. 라는 문제와 의존하고 있는 객체가 생성되지 않았는데도, 의존 받는 객체가 생성될 수 있어서 컴파일 단계에서는 문제가 없지만 런타임 단계까지 가서 순환 참조를 일으킨다. 마지막으로 final 제어자를 사용하지 못하기 때문에 불변성을 해치게 된다.

마지막으로 setter주입 방식이 있다.

그냥 세터 만들 듯 만들어서 필요시에 주입하면된다.

이것의 단점은 누군가 컨트롤러를 호출했을 때 세터메소드가 public으로 열려 있어야 한다. 하지만 서비스는 중간에 바꿀 필요가 없다. 한번 세팅이 되게 되면 변경될 필요가 없다는 뜻이다. 다시 말해 의존관계가 실행 중에 동적으로 변하는 경우는 거의 없다. 근데 public하게 노출되게 되면 잘못 바뀌게 되는 경우가 발생하여 문제가 생긴다.

결국 맨 처음 애플리케이션이 조립될 시점에 주입이 완료되고 변하지 않는 생성자 주입이 최고다.

다시 스프링 빈을 등록하는 방법으로 돌아와서 우리는 앞으로 두번째 방법인 config파일을 통해 자바코드로 스프링 빈을 등록할 것이다. 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하는 경우에 사용하면 좋은 두번째 방법이 우리의 상황과 맞게끔 시나리오(db가 결정되지 않음)를 써놨기 때문이다. 현재는 임시 db인 store를 이용해 데이터를 저장하는 MemoryMemberRepository를 구현하고 사용중인데 실제 db가 결정되면 이 MemoryMemberRepository를 바꿔 끼워야 하기 때문이다.

@Bean

Public MemebrRepository() {

	// Return new MemoryMemberRepository();

	Return new DbMemberRepository();

}

이제 회원기능을 구현해볼거다.

이때 내가 맨 앞에서 웰컴페이지가 우선순위일까 컨트롤러가 우선순위일까 했던 실험했던 내용이 나온다.

스프링은 url이 들어오면 스프링 컨테이너에서 관련 컨트롤러를 먼저 찾고 ‘없으면’ 리소스파일의 스태틱 폴더에서 찾는다. 이는 웰컴페이지도 마찬가지이다. 여기서 그럼 다시 기억해야 하는 내용은 컨트롤러에서 매핑되어 리턴값으로 html파일을 찾는 과정이다. url이 들어오면 스프링은 스프링컨테이너로 들어가 관련 컨트롤러를 찾고 해당 컨트롤러는 리턴값으로 html의 경로를 반환한다. 근데 스프링부트의 기본 구조를 보면 리소시스 폴더 안에 스태틱 폴더와 템플릿 폴더가 있다. 결국 지금까지 한 내용을 보면 컨트롤러는 resources안에 template 폴더를 들어가 리턴값의 제시된 html폴더를 찾아서 작동된다. 고로 template폴더 안에 다른 패키지를 만들었다면 리턴값에는 /다른 패키지 이름/html파일명 을 적어주면 되는것이다. 추가로 리턴값에 “redirect:/” 를 해주는 경우가 있는데 이는 작동이 끝나면 localhost:8080/ 상태로 돌려주겠다는 뜻이다.

게시판 만들 때 보면 <input> 에 선언되는 id와 name이 헷갈렸는데 강의에서 보니 name값으로 매칭되는 변수를 찾아 그곳으로 데이터가 보내지는 것이어따. 근데 궁금한게 name=”name” 으로 선언 한다고 가정했을 때 이 작성된 값이 컨트롤러로 넘어왔을 때 메서드의 매개변수의 객체의 name과 어떻게 매핑되는지였다. 이거를 스프링이 member객체에 있는 세터를 통해 자동으로 name을 찾아서 넣어주는 것이었다.

Html에서 잠깐 알아두고 가고싶은게 있다

<Table> 만들 때 tr, th, td 가 뭔지…

Tr은 tablerow로 테이블 행을 만든다.

Th는 tableheader로 테이블 열의 제목을 만든다.

Td는 tabledata로 테이블 열의 값을 넣는다.

그리고 thymeleaf로 작성할 때 데이터를 받아오는 ${} 는 컨트롤러에서 설정한 model값을 꺼내올 때 사용하는것이니 다시 한번 명심하자.

이때 model의 값으로 들어오는 객체를 보면 즉 현재 예제에서는 Member로 가보면 분명 private으로 다 선언되어있다. 근데 어떻게 받아오느냐 하면 getter와 setter로 잘 받아온다고 한다. 이를 java의 property방식의 접근이라고 하는데 게터세터를 사용해서 값에 접근하는 방식이라고 한다.

이제 서버를 내리면 여태 등록한 데이터가 전부 사라지는 것을 방지?하기 위한 데이터베이스 설정을 다음시간에 배운다.

'Spring' 카테고리의 다른 글

스프링 핵심 원리 - 3  (0) 2024.01.27
스프링 핵심 원리 - 2  (0) 2024.01.24
스프링 핵심 원리 - 1  (0) 2024.01.23
스프링 입문 - 2  (0) 2024.01.22
Spring의 기본 구조  (0) 2024.01.12