[Spring] 스프링부트로 WAS 만들기 1

11507 단어 SpringJavaJava

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

스프링 입문 - 코드로 배우는 스프링 부트, MVC, 웹 DB기술

강의 목표 : 전반적인 스프링 구조 익히기
스프링은 매우 거대하기 때문에 실무에 꼭 필요한 부분만

https://start.spring.io/ 스프링 프로젝트 생성

Maven, Gradle : 라이프 사이클 관리 / 필요한 라이브러리 가져오기
요즘은 모두 Gradle 사용
Java 11버전
Spring Web , Thymeleaf 라이브러리 추가
Gradle을 통해 SpringWeb , thymeleaf와 의존관계에 있는 라이브러리들이 모두 가져온다.
spring-boot-starterweb : 톰캣, 스프링 웹 mvc
thymeleaf : 타임리프 템플릿 엔진 (view)
spring-boot-starter : 스프링부트 + 스프링코어 + 로깅(logback, slf4j)
spring-boot-starter-test : Junit, mockito, assertj, spring-test

build.gradle
Dependencies 내용을 repositories에 mavenCentral() 을 통해 가져온다.
버전관리

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

다음과 같은 페이지가 나오면 정상적으로 프로그램이 구동됐다고 볼 수 있다.

Build and run을 인텔리J로 바꾸면 Gradle을 통해서 실행되는 것보다 훨씬 빠르게 실행 가능하다.
기존 실행 IntelliJ -> Gradle -> Java
변화 IntelliJ -> Java

View 환경설정
Welcome Page 만들기

https://www.thymeleaf.org/

스프링은 양이 너무 방대하기 때문에 필요한 부분만을 잘 찾는 능력이 중요하다.
https://docs.spring.io/spring-boot/docs/current/reference/html/
에 대부분 잘 나와있음

@Controller
public class HelloController {

    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!!");
        return "hello";
    }
}

@GetMapping("hello") 8080/hello 라는 get method를 통해 주소창에 입력되었을때
model.addatrribute("data","hello!!"); hello!!라는 데이터를 모델에 넣고 리턴

ViewResolver가 return "hello" 는 resources 안에 hello.html을 찾아라

처리과정

웹 브라우저 -> http://localhost:8080/hello -> 내장 톰캣 서버 -> HelloController (return : hello , model(data:hello!!) -> viewResolver -> templates/hello.html 리턴

html에서 key값 표시 ${data}

배포 과정

./gradlew build로 프로젝트를 빌드 하여 톰캣 없이 자바만 설치되어 있다면 어디서든지 스프링 부트 서버 실행 가능

libs에 hello-spring-0.01-snapshot.jar 을 실행시켜 우리가 만든 프로젝트 실행 가능

오류나는 경우 ./gradlew clean build를 통해 다시 빌드

웹 개발이란?

정적 컨텐츠

Static Content

MVC와 템플릿 엔진

MVC : Model, View, Controller
이전 JSP에서는 View에서 Controller도 같이했지만 스프링에서 나누어서 구분함

<p th: text="'hello ' + ${name}"> hello! empty</p>

템플릿이 동작하지 않을때는 hello! empty 아닐때는 hello + name으로 치환되어 작동

API

동적 컨텐츠는 다음과 같이 2개의 작업으로 처리해야한다.
1. MVC 처럼 뷰(HTML)를 찾아 템플릿 엔진에서 정보를 변환해 웹 브라우저에 전달
2. API 방식으로 객체(데이터)를 Json 형태로 바로 전달

   @GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name){
        Hello hello = new Hello();
        hello.setName("name");
        return hello;

    }

    static class Hello {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

Json 형식의 key-value 형태로 전달
getter/setter를 이용한 자바 빈 표준 방식을 주로 사용 = Property 접근 방식
@ResponseBody 가 있다면 ViewResolver에 넘기지 않고 HttpMessageConverter에 의해 단순 문자면 StringConverter 를 통해 바로 넘겨줬지만 객체가 반환되었다면 JsonConverter를 통해 Json 방식으로 전달된다

StringConverter = StringHttpMessageConverter
JsonConverter = MappingJackson2HttpMessageConverter
등 여러가지의 기타 HttpMessageConverter가 기본적으로 등록되어 있다.

Http Accept 헤더와 컨트롤러 반환 타입 정보 둘을 조합해 컨버터가 선택된다.

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

비즈니스 요구사항 정리

데이터 : 회원 ID, 이름, 비밀번호
기능 : 회원 등록과 조회
아직 데이터 저장소가 선정되지 않음
데이터에 따라 RDBMS , NOSQL 을 선택해야하지만 현재는 테스트용으로 개발한다고 가정

컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 핵심 비즈니스 로직 구현(회원 중복가입 X와 같은 서비스)
리포지토리 : DB 접근, 도메인 객체를 DB에 저장하고 관리
도메인 : 비즈니스 도메인 객체, 예) 회원 , 주문, 쿠폰과 같은 객체 관리

DB가 선택되지 않아 우선 인터페이스로 구현하여 클래스를 언제든지 변경할 수 있게 설계
데이터 저장소는 RDB, NoSQL 등을 고민중인 상황 우선 H2 메모리기반 데이터베이스 사용

결과 null일때 반환도 되야 하므로 다음과 같이 코드를 작성하는건 옳지 않음
Optional.ofNullable(store.get(id)) value가 존재하지 않다면 null 반환

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long,Member> store = new HashMap<>();
    private static long sequence = 0L;
    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

그렇다면 다음과 같이 작성한 구현체를 어떻게 잘 작동하는지 검증 할 수 있을까?
main 메서드를 통해서 실행하거나 컨트롤러를 통해서 실행하기에는 오래 걸리고 반복 실행하기 어렵다. Java는 Junit이라는 프레임워크를 통해 이러한 문제를 해결한다.

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

    @Test
    public void save() {
        Member member = new Member();
        member.setName("spring");

        repository.save(member);
        Member result = repository.findById(member.getId()).get();

        // 메번 메서드 실행 확인 힘듬 System.out.println("result = " + (result == member));
        //Assertions.assertEquals(member,null);
        assertThat(member).isEqualTo(result);
    }
}

실무에서는 build tool과 assetThat을 엮어 다음단계로 가지 못하게 막아버린다.

Test class를 통해 모든 클래스를 한번에 테스트도 가능하다.
모든 테스트는 순서가 정해져있지 않기 때문에 독립적으로 설계해야한다.
따라서 repository를 테스트 하고나면 항상 초기화를 해주어야 한다.
(테스트 메서드 실행 순서에따라 repository 값이 바뀔 수 있기 때문)

@AfterEach 한 메서드가 끝나고 자동으로 실행되는 콜백메서드를 통해
repository.clear()를 해준다.

테스트를 먼저 개발하고 repository를 개발하면 TDD 테스트 기반 개발
(검증할 수 있는 틀을 먼저 만들고 코드를 구현) 테스트 주도 개발

null일 가능성이 있다면 Optional 객체로 감싸주고 해당하는 메서드를 사용 가능하다.

서비스는 비즈니스에 의존하여 용어 선택
Repository는 기계적으로 개발스럽게 용어 선택

테스트 클래스 에 있는 Repository와 구현 클래스에 있는 Repository 공유하고 싶을때 .. 둘다 new로 객체 생성한다면 다른 Repository를 의미한다.

Constructor를 통해 해결

BeforeEach를 통해 넣어준다. DI (Dependency Injection)

스프링 빈과 의존관계

회원 컨트롤러가 회원서비스와 회원 리포지토리를 사용할 수 있게 의존관계 설정

스프링 빈을 등록하는 2가지 방법

컴포넌트 스캔과 자동 의존관계 설정
자바 코드로 직접 스프링 빈 등록하기

@Service , @Repository , @Autowired 로 자동 의존관계 설정
스프링은 컴포넌트 어노테이션이 있으면 스프링 컨테이너에 객체를 등록 한다.
Autowired는 각 객체를 연결해주는 역할

@Component : 스프링 빈에 자동 등록
위 어노테이션이 포함되어 있는 어노테이션들 @Controller, @Service, @Repository

메인메서드와 같은 패키지나 하위 패키지에 있는 컴포넌트만 스캔을 한다.

스프링은 컨테이너에 스프링 빈을 등록할 때 , 기본으로 싱글톤으로 등록한다.
(유일하게 하나만 등록해서 공유한다.)
따라서 같은 스프링 빈이면 모두 같은 인스턴스다.

자바 코드로 직접 스프링 빈 등록하는 방법
MemberService에 SpringConfigure 클래스를 만들어서 다음과 같이 작성한다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Optional;

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

DI에는 생성자 주입 , 필드 주입 , getter/setter 주입
생성자 주입이 가장 현업에서 주로쓰인다.
의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장
setter 주입은 아무 개발자나 호출할 수 있게 열려있다. public으로 설정해야하기 때문에

실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 어노테이션을 통해 컴포넌트 스캔 방식을 사용한다. 하지만 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야하면 스프링 빈으로 등록한다.
ex)데이터 저장소가 정해지지 않아 리포지토리를 변경해야하는 경우

@Autowired를 통한 DI는 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다 ex)new로 객체 생성

❓❔
궁금한 점 정리
1. Getter/Setter 를 왜 사용할까?
2. Optional은 무엇인가?
3. 동시성문제란 ?
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long,Member> store = new HashMap<>();
다음 코드는 실무에서는 동시성 문제로 ConcurrentHashMap 사용해야한다.
DB에 저장되는 객체는 atomic long 등으로 바꿔줘야한다.
4.BeforeEach, AfterEach ?
5.JavaStream
6.예외 처리

좋은 웹페이지 즐겨찾기