BackEnd/Spring

Spring 입문 4 ~ 6주차

Bubbles 2022. 8. 5. 12:58

📌 스프링 빈과 의존관계

📌 Component scan 

- @Component 어노테이션이 붙은 코드를 스캔하여 스프링 컨테이너 빈으로 등록하는 것

- 강의에서 사용한 @Controller, @Service, @Repository등이 @Component의 특수화된 케이스들. 

- Application 파일 하위부터 컴포넌트 스캔이 들어가므로 default 설정은 패키지 포함 하위만 스캔하며, @ComponentScan 어노테이션을 표기해두면 상위에서도 컴포넌트 스캔 하도록 지정이 가능 

- 스프링 컨테이너가 @Controller 보고 객체로 생성하여 관리해준다. 스프링 실행 시점에 이 @Controller 객체를 생성하여 스프링 컨테이너가 관리 

- 위에 적혀있듯 Controller는 요청을 받는 측, Service가 비즈니스 로직들 처리, Repository가 내부 로직 처리

 

@Controller
public class MemberController {
 private final MemberService memberService;
 @Autowired
 public MemberController(MemberService memberService) {
 	this.memberService = memberService;
 }
}

 

📌 @Autowired

실행 시점에 스프링 컨테이너가 생성자에 이 어노테이션이 붙어있다면 이 매개변수에 있는 memberService 연결을 자동으로!  해준다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection, 의존성 주입이라고 함) 

 

📌 DI 

 

- 참조중인 memberService는 아직 순수 자바 코드이기 때문에 이 파일도 스프링 컨테이너가 관리하도록 해줘야 함. 

 

🤔 스프링 빈 주입 방법? 

- @Autowired : 생성자 빼고 필드에 바로 주입 (중간에 내가 변경하기 어렵다는 단점이 존재)

- setter (setter가 public하게 노출되며 처음 세팅외에는 호출될 일이 없는데 잘못 호출될 경우 문제 발생)

- 💚생성자 사용(@RequiredAgrsConstructor : final이 선언된 모든 필드를 인자값으로 하는 생성자 자동생성 라이브러리)

         -> 실행 시 한 번 호출되고 끝나서 의존관계 주입이 동적으로 변하는 경우는 거의 없기 때문. (이런 경우는 config파일               자체 수정, 서버 다시구동)

 


@Service
public class MemberService {
 private final MemberRepository memberRepository;
 @Autowired
 public MemberService(MemberRepository memberRepository) {
 this.memberRepository = memberRepository;
 }
}

📌 스프링 빈 등록할 때는 기본적으로 싱글톤으로 등록(하나만 등록, 모두 같은 인스턴스)한다. 위 그림에서 예를들어, orderService처럼 다른 서비스가 memberRepository참조한다. 이러면 전에 만들어진 memberRepository(이미 스프링 컨테이너에서 생성된)애를 전달해줌. 

 


📌 이 외에 방법으로 자바 코드로 직접 스프링 빈으로 등록하는 방법이 존재 (@Configuration) 이 방법이 더 권장

- Spring Config등 파일을 생성하고 @Configuration어노테이션 붙이기
- 그리고 @Bean어노테이션 등록

 

@Configuration이 스프링 컨테이너에게 설정정보를 전달하는 어노테이션, 그리고 각 @Bean이 빈으로 등록할 객체들에 붙는 어노테이션

 

💚 추가로, @Configuration 없이 @Bean만 사용한다면 빈 등록은 가능한데 싱글톤은 깨진다. ( EX 위의 memberRepository() 처럼 의존관계 주입이 필요해서 메소드를 직접 호출할 때 ) 그러니 항상 별다른 고민 없이 @Configuration 어노테이션을 통해 스프링 설정정보를 관리하자. 

 

📌 왜 자바 코드로 짜는게 더 권장되는가? 

EX) 상황에 따라 구현 클래스를 변경해야 한다(기획안이 픽스되지 않은 경우에)
-> config클래스만 뭐 주입할지 살짝 바꿔두면 됨(인터페이스 파일을 구현하는 클래스들이라 호환 가능)
-> component 스캔보다 config하나 바꾸는게 간단해서 ! 

 

📌 주의사항
스프링 빈으로 등록해놔야 autowired가 작동된다. new로 직접 객체를 생성하는 경우에도 autowired X!
autowired는 컨테이너가 관리해야만 동작하는 것을 기억. 

 


📌 타임리프 이용한 화면 기능들 구현

📕 controller로 요청이 들어가 전달된다면 도메인에서 리소스를 찾고  index.html 과 같은 정적 파일들은 무시됨.

@GetMapping(value = "/members/new")
 public String createForm() {
 return "members/createMemberForm";
 }
 
@PostMapping(value = "/members/new")
public String create(MemberForm form) {
 Member member = new Member();
 member.setName(form.getName());
 memberService.join(member);
 return "redirect:/";
}

📕 참고로 저 value에서 members가 공통된 경로니까 Controller 맨 위에 빼는 것도 가능 

@RequestMapping(value = "/user", consumes = MediaType.APPLICATION_JSON_VALUE)
public class UserApiController {
    private final UserService userService;
    @PostMapping("join")
    public ResponseEntity join(@RequestBody UserJoinRequestDto requestDto) {
        try {
            UserJoinResponseDto userJoinResponseDto = userService.join(requestDto);
            return ResponseEntity.ok().body(ResponseDto.res(Status.OK, Message.JOIN_SUCCESS, userJoinResponseDto));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(ResponseDto.res(Status.BAD_REQUEST, e.getMessage()));
        }
    }

📕 아래 코드에 있는  ${members} : model.addAttribute 
-> 이게 루프를 돌면서 (th가 타임리프 반복문) 리스트에 있는 모든 객체들을 멤버에 담고 (for each처럼) id, name 출력해줌. 

<div class="container">
 <div>
 <table>
 <thead>
 <tr>
 <th>#</th>
 <th>이름</th>
 </tr>
 </thead>
 <tbody>
 <tr th:each="member : ${members}">
 <td th:text="${member.id}"></td>
 <td th:text="${member.name}"></td>
 </tr>
 </tbody>
 </table>
 </div>
</div>

📌 스프링 DB 접근 기술

drop table if exists member CASCADE;
create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

DB ~ long : bigint, string : varchar로 변경! (데이터 타입이 약간 다름)

📌 참고로 mysql 계열의 db에서는 auto_increment라는 옵션을 통해 id값이 자동 생성, 증가하게끔 만든다. 

📌 DDL : 데이터베이스 정의어 (create, drop 등)

 


📒 순수 JDBC

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

-> build.gradle

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

/////////////////////
spring.datasource.url = 여기다 rds 주소같은거 올림.
spring.datasource.driver-class-name = mysql, mariadb등 디비 종류 따라 다름

-> application.properties

📌 참고로 요즘은 application.yml 방식으로 (계층 구조로 작성 가능) 사용한다. 이렇게 설정정보 담는 파일(db url, key등등... )은 깃에 안올라가도록 잘 관리해야함.

📌 EC2, RDS, S3등 AWS설정정보 등도 여기다 기입해둔다. 

 

JDBC 사용방식 

@Configuration
public class SpringConfig {
 private final DataSource dataSource;
 public SpringConfig(DataSource dataSource) {
 this.dataSource = dataSource;
 }
 ...

- DataSource사용하도록 Spring Config 파일 변경

- DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둬서 DI를 받을 수 있다

public Member save(Member member) {
 String sql = "insert into member(name) values(?)";
 Connection conn = null;
 PreparedStatement pstmt = null;
 ResultSet rs = null;
 try {
 conn = getConnection();
 pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
 pstmt.setString(1, member.getName());
 pstmt.executeUpdate();
 rs = pstmt.getGeneratedKeys();
 if (rs.next()) {
 member.setId(rs.getLong(1));
 } else {
 throw new SQLException("id 조회 실패");
 }
 return member;
 } catch (Exception e) {
 throw new IllegalStateException(e);
 } finally {
 close(conn, pstmt, rs);
 }
 }

- 코드 일부만 발췌해옴. Connection 등의 자원들은 사용후에는 꼭 반납. (close) 실제로 연결하는 것이기 때문이다. 
- tmi)  connectionpool 만들어서 사용도 가능, 미리 일정량의 connection 을 생성해두고(pool) 그때그때 가져다 쓰고 pool에 다시 반납하는 형식 

 

📌 객체 지향적인 설계는 코드 수정을 최소화해서 좋다. 
- 다형성을 활용한다.   

 

- SOLID 원칙

📌 SRP : 단일 책임 원칙 한 클래스는 하나의 책임만 가진다 하나의 책임은 약간 모호(상황따라 다름), 변경이 있을 때 파급 효과가 적은가를 본다.

📌 OCP : 개방-폐쇄 원칙 (가장 중요!) SW 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 한다. 구현을 하나 더 하는 것은 확장하는 것, 하지만 기존 코드의 변경은 X 객체를 생성하고, 연관관계를 맺어주는 것을 Service에서 하지 않고 스프링 컨테이너가!

📌 LSP : 리스코프 치환 원칙 상위 타입의 인스턴스는 하위 타입의 인스턴스로 바꿀 수 있어야 한다. (컴파일 성공을 넘어서서!)

📌 ISP : 인터페이스 분리 원칙 특정 클라이언트를 위한 인터페이스 여러 개가 하나의 범용 인터페이스보다 낫다.

📌 DIP : 의존관계 역전 원칙, 프로그래머는 추상화에 의존, 구체화에 의존하면 안된다. 구현 클래스에 의존하지 말고, 인터페이스에 의존하자(언제든 갈아 끼울 수 있게)

 


@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
 @Autowired MemberService memberService;
 @Autowired MemberRepository memberRepository;
 @Test
 public void 회원가입() throws Exception {
 //Given
 Member member = new Member();
 member.setName("hello");
 //When
 Long saveId = memberService.join(member);
 //Then
 Member findMember = memberRepository.findById(saveId).get();
 assertEquals(member.getName(), findMember.getName());
 }
 @Test
 public void 중복_회원_예외() throws Exception {
 //Given
 Member member1 = new Member();
 member1.setName("spring");
 Member member2 = new Member();
 member2.setName("spring");
 //When
 memberService.join(member1);
 IllegalStateException e = assertThrows(IllegalStateException.class,
 () -> memberService.join(member2));//예외가 발생해야 한다.
 assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
 }
}

📒테스트라서 @Autowired로 간단하게 넣어버림! 

📒 @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다. (통합 테스트 용 어노테이션)

📒 @Transactional
- insert하기 전까지는 commit이 반영이 아님. 
- test 끝나고 rollback해버리기! -> 굳이 db에 테스트 넣은거 일일히 delete하는 것보다 낫다. 
- test에 날리면 rollback해준다. 
테스트 시작 전 트랜잭션을 걸고, 끝나면 커밋이 아닌 rollback을 해줌. 

 

📒 트랜잭션? (추가)
DB상태 변화시키는 논리적 기능 수행하기 위한 작업의 단위를 의미, SQL 문으로 접근 하는 것이 db변화하는것
작업 : 일련의 연산, 사람이 정함, DB안정성 확보가능
- Commit연산 : 성공적으로 끝나서 db가 일관성있는 상태일때
- Rollback : 비정상종료, 원자성 깨져서 재시작, undo상태로 돌리는 연산

 

📒 테스트 

단위테스트 : 순수 자바 코드로 진행하는 테스트, 통합테스트 : 스프링 띄워서 db연결하고 진행
단위테스트가 좋은 테스트일 확률이 높다. 컨테이너까지 올려야하는 상황이면 테스트 설계가 잘못되었을 확률이 높다. 살다보면 통합테스트도 필요하지만, 단위테스트를 잘 만드는 것이 좋다. 

📒 단위테스트 (Unit Test)는 기능 단위의 테스트 코드를 작성하는 것을 의미

 

 

📌 Spring JDBC template

@Override
public Member save(Member member) {
 SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
 jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
 Map<String, Object> parameters = new HashMap<>();
 parameters.put("name", member.getName());
 Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
 member.setId(key.longValue());
 return member;
}

📌 simpleJdbcInsert : 쿼리 짤 필요 없이 테이블 명이랑 pk가지고 insert문 만들어주고, 쿼리의 결과 rowMapper통해 매핑.. (객체로 변경해줌) 

🤔 하지만 여전히 쿼리는 개발자가 짜야하잖아? 


JPA등장 : sql과 데이터 중심 설계에서 객체 중심의 설계로 패러다임 전환 가능

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

- jpa가 테이블을 자동으로 만들어주는데 우리는 이미 테이블 만들어서 none으로 꺼둔다. 
- JPA가 인터페이스, 구현체가 hibernate라고 생각. 자바 진영의 표준 인터페이스, 구현은 여러 업체들이 한다고 생각하자

 

📌 ORM : object relation mapping 객체 - 관계형 데이터베이스 매핑

보통 CRUD는 기본적으로 구현되어 있음, pk기반아닌 다른 것들은 jpqa라는 쿼리 작성해줌.

DB저장하거나 안하거나 할 때 (db접근이 들어갈 때) transaction어노테이션 (MemberService 파일에) 부여. 

import org.springframework.transaction.annotation.Transactional
@Transactional
public class MemberService {}


📌 entity 매핑

package hello.hellospring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Member {
 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 private String name;
 public Long getId() {
 return id;
 }
 public void setId(Long id) {
 this.id = id;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
}
public class JpaMemberRepository implements MemberRepository {
 private final EntityManager em;
 public JpaMemberRepository(EntityManager em) {
 this.em = em;
 }
 public Member save(Member member) {
 em.persist(member);
 return member;
 }
}

- 굉장히 코드가 간결해짐! 



📌 스프링 데이터 JPA는 JPA를 편리하게 사용하기 위한 도구

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,
Long>, MemberRepository {
 Optional<Member> findByName(String name);
}


📌 interface생성, JpaRepository<entity class, pk타입> 상속받기 : 알아서 구현체 만들어서 스프링 컨테이너에 등록해줌. 그냥 가져다 쓴다. 

📌 인터페이스 구현시에 (JPA상속받는) 규칙
- findByName(String name) 
이러면 jpql이 select m from Member where m.name = ? 이런식으로 자동으로 만들어주고 sql로 번역됨

- findBy XXX and XX 이런 식의 규칙으로 생성 가능. 


인터페이스 이름만으로도 개발이 끝난다는 혁신적인 방법...! 대부분의 단순한 케이스들은 interface만으로도 끝남. 
QueryDsl 사용하면 동적 쿼리 사용가능. @Query로 네이티브 쿼리 (SQL), jdbc 템플릿, mybatis등을 섞어서 사용가능!