스프링 입문 정리

고라니·2021년 8월 3일

모든 공부 지식의 출처는 inflearn - 스프링 입문 강의 (김영한님) 입니다.^^

document

기본적으로 spring.io -> proejct - > spring-boot -> 버전에 맞는 docs
가서 내가 필요한 것들을 검색하고 찾고 이용하는 것을 연습해야 한다.
(스프링 너무 방대하게 많은것들을 제공하기 때문에 필요한 것을 찾아야 함)

기본적인 Controller 동작


출처 : 인프런 김영한님 스프링입문 강의 (최고)

  • 먼저 url 로 /hello 를 브라우저에 요청하면 내장 톰캣 서버에서 자동으로 @GetMapping("hello") 가 있는 컨트롤러를 찾아서( 여기서는 HelloController )
    컨트롤러 리턴 값으로 문자를 반환하면 viewResolver가 화면을 찾아서 처리하게 된다.
  • 기본적으로 스프링 부트 템플릿엔진 기본 viewName 매핑
  • ( resources:templates/ +{ViewName}+ .html )
#### controller/helloController.java ####
@Controller
public class HelloController {
   @GetMapping("hello")
   public String hello(Model model) {
   model.addAttribute("data", "hello!!");
   return "hello";
   }
}

#### resource/templates/hello.html ####
<body>
  <p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>

스프링 정적 컨텐츠 동작

웹브라우저 url로 요청이 들어가면? ex) localhost:8080/hello-static.html
-> 내장 톰캣 서버가 요청을 받고 스프링 컨테이너에 넘겨줌
-> 스프링 컨테이너는 컨트롤러에서 먼저 찾음 (우선순위를 가짐)
-> 컨트롤러에 없다면 resources; static/hello-static.html 을 찾음. 있으면 넘겨줌~

MVC 와 템플릿 엔진 에서의 동작 (Model, View, Controller)

View는 화면을 그리는데 집중 해야하고, 컨트롤러,모델은 비즈니스 로직,내부처리에 집중해야한다.

동작

웹브라우저에서 localhost:8080/hello-mvc?name=spring! 요청하면 내장톰캣서버를 먼저 거침
-> 스프링 컨테이너에서 helloController의 매핑되어있네 하고 찾고
해당 메소드를 호출해주면서 return "hello-template"; 함.

    @GetMapping("hello-mvc")
    public String helloMvc(@RequestParam("name") String name, Model model) {
        model.addAttribute("name", name);
        return "hello-template";
    }
    
    <body>
	<p th:text="'hello ' + ${name}">hello! empty</p>
    </body>

model에는 name:spring! 으로 넣어주고 바로 viewResolver가 동작함 (화면과 관련된 해결자 동작함)
view를 찾아주고 템플릿 엔진을 연결시켜줌
viewResolver는 resource에 있는 templates/hello-template.html 을 찾아서Thymeleaf 템플릿엔진에게 처리해달라고 요청하고, 템플릿엔진이 변환후 웹브라우저에 넘겨줌.

API 방식 ( json 같은걸로 서버끼리 넘기는..방식들)

@GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) {
        Hello hello = new Hello();
        hello.setName(name);
        return hello;
    }//찐 API방식
    
    static class Hello {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    
    http://localhost:8080/hello-api?name=spring!!
    결과화면 (json형식으로 화면에 보여줌)
    {"name": "spring!!"}

@ResponseBody 어노테이션은 왜썼을까?

해당 어노테이션 을 만나면 기존 mvc방식처럼 동작하는것이 아니고 (ViewResolver X)
httpMessageConverter가 동작하는데 (http응답에 이 데이터를 그대로 넣어야겠다 하고 동작)
세부적으로 문자 / 객체 둘의 컨버터가 다름. (우리는 객체를 주의깊게 살펴보면 됨.)
return: hello(name:spring!) 이렇게 객체가 오면 JsonConverter가 동작하고
return: "hello" ! + name 이런식으로 문자열이 오면 StringConverter가 동작함.

출처 : 인프런 김영한님 스프링 입문강의 (최고)

TEST

test/java 폴더의 모든 테스트는 각각의 메소드에 대하여 독립적이어야한다. (한번에 돌렸을 때
각 테스트가 서로의 테스트에 영향주지 않도록 해야함)
따라서 각 테스트 끝날때마다 데이터를 clear해줘야함. @AfterEach (메소드 끝날때마다 동작하게 하는 애노테이션)
콜백같은거임.

test case 작성 시 given / when / then 주석을 활용해서 만들면 더욱 쉽고 정확하게만듬
(이런게 주어졌고(이런 데이터기반으로), 이것을 실행하면(검증하려는것) , then에서 검증한다

-스프링과 빈의 의존관계-

  • 스프링 컨테이너가 @Controller애노테이션 MemberController 객체만들어서 컨테이너에 넣어둠 .이것이 Bean이 관리된다고 하는것이다.

  • @Controller, @Repository, @Service 이렇게 @Component를 달고있는 애노테이션을 만나면 스프링이 객체로 생성하여 컨테이너에 등록해놓음.

  • Autowired는 그림의 연관관계를 선처럼 연결해줌. Controller가 memberService를 쓸수있게 해주고, Service가 Repository를 쓸 수 있게 해줌.
    (@Autowired는 연결관계 매칭, 이게 자동의존관계설정)
    생성자에 @Autowired 있으면 ? 컨트롤러 생성될때 스프링빈에있는 컨테이너가 가진 애랑 자동으로 매칭해줌 (바깥에서 스프링이 넣어준거니까 DI)

  • 이것이 컴포넌트 스캔과 자동 의존관계 설정이라고 한다.

  • 아무 패키지나 만들고 거기에 컴포넌트 애노테이션 붙이면 되나요? -> 기본적으론 안됨. 기본적으로 스프링은 hello.hellospring 포함한 하위 패키지를 다 뒤져서 등록함

    memberService 와 memberRepository 가 스프링 컨테이너에 스프링 빈으로 등록되었다.

참고 생성자에 @Autowired 를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다. 생성자가 1개만 있으면 @Autowired 는 생략할 수 있다.

@Controller
public class MemberController {
    //스프링 컨테이너가 @Controller애노테이션 MemberController 객체만들어서 컨테이너에 넣어둠
    //이것이 Bean이 관리된다고 하는것이다.
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
        // Autowired : 스프링이 스프링이 컨테이너의 memberService와 연결해줌
    }
}

컴포넌트 스캔 원리

  • @Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.

  • @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.

  • @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.

    • @Controller
    • @Service
    • @Repository

참고: 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다.
설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

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

기존 컴포넌트 스캔인 회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 애노테이션을 제거하고 진행할 것.

최상위인 컨트롤러는 @Controller , @Autowired 그대로 사용
WHY? 스프링은 @Controller 애노테이션이 있어야 스프링의 MVC 컨트롤러로 인식할 수 있습니다.
그리고 스프링 빈으로 이 컨트롤러를 등록해야 하는데요.
현재 스프링 부트를 사용하고 있는습니다. 스프링 부트는 컴포넌트 스캔 기능이 기본으로 동작합니다.
그런데 컨트롤러는 어차피 @Controller가 있기 때문에 컴포넌트 스캔으로 스프링 컨테이너에 자동 등록됩니다. 따라서 별도로 등록하지 않으셔도 됩니다.

SpringConfig.java 따로 만들어서 @Configuration 달아주고 @Bean으로 스프링컨테이너에 넣기

(참고~: @Autowired //단독으로 있으면 생략가능함)
DI는 필드주입,setter주입은 별로 권하지않음 (개발은 최대한 변경하면안돼서 맨처음에 스프링 조립할때
한번 만드는게 좋은데 setter는 public이라 외부접근으로 인해 바뀔가능성이있고, 필드주입은 그냥 너무 코드가 수동적이라 비추)

자바코드로 빈을 등록할 때 SpringConfig 클래스에 작성을 하는거면 SpringConfig 파일이 스프링 컨테이너의 역할을 하는건가요?
--->스프링 컨테이너가 SpringConfig 파일을 읽어서 스프링 컨테이너 내부에 스프링 빈을 생성합니다. 따라서 SpringConfig는 스프링 컨테이너가 사용하는 설정정보로 사용됩니다. 과거에는 xml을 설정정보로 사용했는데, 지금은 자바 코드를 설정정보로 사용한다고 이해하시면 됩니다.

참고: 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.

주의: @Autowired 를 통한 DI는 helloConroller , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.

참고: DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.

왜 생성자 주입 방법을 권장할까?

  1. 순환 참조를 방지할 수 있다.
    개발을 하다 보면 여러 컴포넌트 간에 의존성이 생긴다. 그중에서도 A가 B를 참조하고, B가 다시 A를 참조하는 순환 참조도 발생할 수 있는데
    실제로는 이러한 형태가 되어서는 안되며, 직접적으로 서로를 계속해서 호출하는 코드는 더더욱 안된다. “순환 참조가 되면 이럴 수도 있구나~”라고 생각하자.
    문제는 애플리케이션이 아무런 오류나 경고없이 구동된다는 것이다. 실제 코드가 호출되기 전까지 문제를 발견할 수 없다. 그렇다면 생성자 주입을 사용한 경우는
    실행 결과는? BeanCurrentlyInCreationException이 발생하며 애플리케이션이 구동조차 되지 않는다. 따라서 발생할 수 있는 오류를 사전에 알 수 있다.
    실행 결과에 차이가 발생하는 이유는 무엇일까? 생성자 주입 방법은 필드 주입이나 수정자 주입과는 빈을 주입하는 순서가 다르다.

먼저 수정자 주입(Setter Injection)을 살펴보면 우선 주입(inject) 받으려는 빈의 생성자를 호출하여 빈을 찾거나 빈 팩터리에 등록한다. 그 후에 생성자 인자에 사용하는 빈을 찾거나 만든다. 그 이후에 주입하려는 빈 객체의 수정자를 호출하여 주입한다
다음으로 필드 주입(Field Injection)을 알아보자. 수정자 주입 방법과 동일하게 먼저 빈을 생성한 후에 어노테이션이 붙은 필드에 해당하는 빈을 찾아서 주입하는 방법이다. 그러니까, 먼저 빈을 생성한 후에 필드에 대해서 주입한다.
마지막으로 생성자 주입(Constructor Injection)은 생성자로 객체를 생성하는 시점에 필요한 빈을 주입한다. 조금 더 자세히 살펴보면, 먼저 생성자의 인자에 사용되는 빈을 찾거나 빈 팩터리에서 만든다. 그 후에 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출한다. 즉, 먼저 빈을 생성하지 않는다. 수정자 주입과 필드 주입과 다른 방식이다.

그렇기 때문에 순환 참조는 생성자 주입에서만 문제가 된다. 객체 생성 시점에 빈을 주입하기 때문에 서로 참조하는 객체가 생성되지 않은 상태에서 그 빈을 참조하기 때문에 오류가 발생한다.

정리

1.순환 참조를 방지할 수 있다.
-순환 참조가 발생하는 경우 애플리케이션이 구동되지 않는다.
2.테스트 코드 작성이 편리하다.
-단순 POJO를 이용한 테스트 코드를 만들 수 있다.
3.나쁜 냄새를 없앤다.
-조금 더 품질 좋은 코드를 만들 수 있다.
4.immutable 하다.
-실행 중에 객체가 변하는 것을 막을 수 있다.
-오류를 사전에 방지할 수 있다.
(https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection)

스프링 통합 테스트

  • 스프링 컨테이너와 DB까지 연결한 통합 테스트를 말함.

  • 보통 스프링 컨테이너까지 올리는 스프링 통합 테스트보다 안올려도 되는 단위테스트가 더 좋은 테스트일 확률이 높기 때문에 안쓰면서 훈련하는 게 좋다고 한다.

  • 테스트 시 필드 Autowired를 쓰면 편하고 좋다.
    (어차피 test단은 끝단이라 한번 테스트하고 끝이기때문에 어디서 갖다쓰거나 엉킬일이 없으므로 굳이 생성자로 할 필요는 없음.)

JDBC 연결 후 스프링 통합 테스트 진행
@SpringBootTest & @Transactional 추가
@SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional : 추가하면 데이터베이스 테스트 끝나면 트랜잭션부분 (시작전)으로 롤백해준다. 이것을 주석 처리하면 테스트 결과가 db에 남는것을 확인할 수 있다.

JPA

  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
  • build.Gradle, applcation.properties 설정 추가해야 할 것들은 PDF를 참고하자.
  1. 도메인 클래스에 @Entity 애노테이션 추가 : //jpa가 관리하는 Entity임을 명시해서 관리하게함.
  • @Id : PK명시
  • @GeneratedValue(strategy = GenerationType.IDENTITY) : DB 자동생성 전략을쓴단뜻
  • //@Column(name = "username") table 자동 생성 할때 쓰는 column 애노테이션
###domain.Member.java###
@Entity
public class Member {
    //pk를 DB가 알아서 생성해주는 방식 : identity방식
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    //@Column 예제에선 이미 테이블생성했으므로 사용x
    private String name;
    
    Getter&Setter
}
  • 레포지토리 안에는 EntityManager 변수를 선언(관리용).
    실행하면 자동으로 스프링컨테이너가 객체 생성.
  • 그리고 데이터를 만져야 할 때 (수정,추가) @Transactional를 붙여줘야돼서 MemberService에 붙여준다. (오류 나면 롤백을 하고, 이상 없으면 적용 시키게 된다.)
repository.JpaMemberRepository.java
public class JpaMemberRepository implements MemberRepository{

    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) { this.em = em;   }

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    } //pk 조회 (무조건 하나일거기 때문에 가능한 일)

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where m.name = :name ")
                .setParameter("name", name)
                .getResultList();
// setParameter "name" 은 쿼리에 :name 에 맵핑됨
        return result.stream().findAny();
    } // JPQL 쿼리언어 사용(pk아닌애들)  객체를 대상으로 쿼리를 날리면 알아서번역

    @Override
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

스프링 JPA

레포지토리는 다중상속하여 인터페이스로 만들고, SpringConfig로 memberRepository 만들면 애노테이션 필요없이 자동으로 구현체 만들고 스프링 Bean에 등록함. 바로가져다쓰면됨.

profile
공부를 열심히 하는 학부생

0개의 댓글