-
Spring 입문 4 ~ 6주차BackEnd/Spring 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등을 섞어서 사용가능!'BackEnd > Spring' 카테고리의 다른 글
Spring 입문 7~8주차 (0) 2022.08.12 Spring Component Scan (0) 2022.08.07 Spring Container (0) 2022.07.31 Spring 입문 강의 섹션 0 ~ 섹션 3 (0) 2022.07.29 Spring Study 3주차 8장 (0) 2022.05.01