좋은 코드란? 새로운 기능을 추가하더라도 구조의 변경이 없어야한다 의존성을 최소화해야함 중복을 제거 코드를 처음 보는사람도 쉽게 해석할 수 있어야함
의존성이란?
public class Consumer {
void eat() {
Chicken chicken = new Chicken();
chicken.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat();
}
}
class Chicken {
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
위 코드에서 consumer는 chicken과 강하게 결합되어있다 왜냐하면 consumer가 chicken이아닌 pizza를 먹고싶다면 내부 메서드를 수정해야하기 때문이다
이를 강하게 결합했다고 하며, 좋지않은 구현이다 결합을 약하게 하는 방법은??
자바의 인터페이스를 사용한다
public class Consumer {
void eat(Food food) {
food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.eat(new Chicken());
consumer.eat(new Pizza());
}
}
interface Food {
void eat();
}
class Chicken implements Food{
@Override
public void eat() {
System.out.println("치킨을 먹는다.");
}
}
class Pizza implements Food{
@Override
public void eat() {
System.out.println("피자를 먹는다.");
}
}
인터페이스를 사용하여 eat메서드를 만들면 customer에서는 코드의 수정이 많이 일어나지 않는다
이런 상태를 약한 결합이라고 한다
주입이란? 필요로하는 객체를 해당 객체에 전달하는것 이다. 필드에 직접 주입
public class Consumer {
Food food;
void eat() {
this.food.eat();
}
public static void main(String[] args) {
Consumer consumer = new Consumer();
consumer.food = new Chicken();
consumer.eat();
consumer.food = new Pizza();
consumer.eat();
}
}
필드에 직접 주입하는 방법이 있고
메서드를 통한 주입
public void setFood(Food food) {
this.food = food;
}
메서드를 통한 주입이 있다.
생성자를 통한 주입
public Consumer(Food food) {
this.food = food;
}
consumer 생성자를 불러올때 Food 객체도 불러온다는 뜻이다
제어의 역전이란? customer가 food를 호출하기떄문에 새로운 food를 만들면 코드를 변경했어야했다 그때의 제어의 흐름은 customer > food 였다.
그것을 해결하기위해 food를 customer에 전달하는 방식으로 customer은 food가 어떤것이 되어도 먹을수 있다. 제어의 흐름이 food > consumer로 역전이 되었다고 한다.
IoC & DI 적용하기
현재 프로젝트의 문제점?
MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
service에서 보면 위 코드가 중복되는것을 알 수 있다
private final JdbcTemplate jdbcTemplate;
public MemoService(JdbcTemplate jdbcTemplate)
{
this.jdbcTemplate = jdbcTemplate;
}
중복되는 코드를 줄이고자 위코드에서
private final MemoRepository memoRepository;
public MemoService(JdbcTemplate jdbcTemplate)
{
this.memoRepository = new MemoRepository(jdbcTemplate);
}
위처럼 생성자에서 주입을 한 후 정리를한다.
controller도 위와같이 수정한다.
위처럼 구현을 하게되면 메모서비스가 메모레파지토리를 만들고있다 제어의 흐름 : memocontroller > memoservice > memorepository로 가고 있다.
강하게 결합이되어있다!
약하게 결합으로 바꿔보자
public MemoController(MemoService memoService)
{
this.memoService = memoService;
}
private final JdbcTemplate jdbcTemplate;
public MemoRepository(JdbcTemplate jdbcTemplate)
{
this.jdbcTemplate = jdbcTemplate;
}
직접적으로 jdbctemplate를 사용하는 repository만 남겨놓고, controller와 service는 위 처럼 없애도 된다. 미리 만들어진 객체를 참조한다는 느낌으로.
다만 의존성 주입은 객체의 생성이 우선되어야한다 위 객체의 생성은 누가해주지??
BEAN 일반 클래스를 스프링이 관리하는 bean 객체로 만들기 위한 어노테이션 @Component을 사용한다 스프링이 run될때 ioc컨테이너가 해당 클래스를 bean객체로 만든다.
어떻게 위 어노테이션만 적용하면 등록해줄까? @SpringBootApllication은 Component 어노테이션을 스캔해주는 어노테이션을 가지고있다. 따라서 @Component 어노테이션을 달고있는 모든 클래스는 Bean객체로 등록해준다.
강한 결합을 약한 결합으로 바꾸기위해 위처럼 선언했다. 하지만 Controller에서 위와 같은 오류가 뜨고 있었다.
외부에서 의존성을 주입한다면 스프링에서 어노테이션을 적용해줘야한다.
@Component
public class MemoService
memoService에 Component 어노테이션을 적용한다
그럼 controller에서는 오류가 사라지는 것을 볼 수 있다. MemoService가 bean객체로 등록이 되어서 문제가 해결된것을 볼 수 있다.
그럼 또 Service가 문제가 생기는데 Repository도 Bean 객체로 등록하기 위해서 어노테이션을 위와같이 적용한다
bean으로 등록되면 끝나나? 아니다. 주입받는 생성자에다가 AutoWired를 사용해야한다, 단 클래스의 생성자가 하나일때만 생략이 가능하고, 생성자가 많을때는 @AutoWired를 사용해야한다.
위에서 의존성 주입은 객체의 생성이 우선되어야 한다고했다 하지만, 객체를 생성하지 않은 상태에서 실행 초기시점에 어떻게 만들어 질 수 있을까? 우선 스프링을 시작하면 @SpringBootApllication은 실행되면서, @Component를 찾아서 Bean으로 등록을한다. 찾아진 Component의 @AutoWired를 찾아서 의존성 주입이 필요한 생성자에 Bean객체를 등록해준다.
따라서 우선 Bean으로 등록이 되어야지 객체를 주입할수 있는것이다. @Component가 없으면 @AutoWired를 사용할 수 없다는뜻!
3Layer Annotation @Controller, @RestController @Service @Repository 라는 어노테이션이 있다
위 어노테이션이 있어도 자동으로 빈을 등록해주며, 스프링에서 관리를 해준다
어떻게?
Controller 어노테이션을 보자 보면 Controller 어노테이션 위에 @Component 어노테이션이 부착되어 있는것을 볼 수 있다. 그래서 가능하다 ㅇㅇ.
따라서 @Controller, @RestController @Service @Repository 를 사용하여 어노테이션을 하면 @Component 어노테이션을 한것과 같다.
EntityManagerFactory EntityManagerFactory는 db에 하나만 생성되어 애플리케이션이 동작하는동안 사용된다 비용이 많이드는 객체이기때문에 꼭 하나만 생성되는것을 권장한다. DB에 대한 정보를 등록해줘야하는데 PERSISTENCE.XML에 정의되어있다.
<persistence-unit name="memo">
persistence.xml 파일을 확인해보면 persistence unit에 memo 라고 되어있다
emf = Persistence.createEntityManagerFactory("memo");
em = emf.createEntityManager();
Persistence클래스의 createEntityManagerFactory의 unit의 name을 매개변수로받아 createEntityManager()를 통해 entityManager를 생성한다.
entiyManager은 스레드 간의 공유가 되지 않으며, 트랜잭션단위로 생성 및 종료가된다.
JPA의 트랜잭션 트랜잭션이란? DB의 무결성과 정합성을 유지하기위한 하나의 논리적 개념이다 트랜잭션 안의 모든 SQL문이 정상적으로 실행되어야 DB에 반영이되지만. SQL문이 하나라도 실패한다면 되돌린다 위와같이 INSERT문을 날려도 DB에는 업로드가 되지않는다.
커밋을 하는 순간 (트랜잭션이 실행되는 도중에 문제가 없을 경우에) 업로드가 되는것을 볼 수 있다.
기본키 조건에 위배되는 중복되는 id를 삽입하였을 경우에는 위와같이 오류가뜬다 3번라인과 4번라인은 정상적으로 실행됐지만, 5번라인에서 멈춰서게 된것이다.
jpa도 위와 유사하게 작동한다. 변경이 발생한 엔티티의 객체들의 정보는 jpa의 영속성 컨텍스트에 저장되고, jpa는 이러한 변경사항을 커밋될때까지 지연시켜둔다.
위처럼 트랜잭션을 사용하지않고 db수정 관련 작업을 수행한다면 트랜잭션이 없다는 경고문이 뜨게된다.! select는 예외
Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.
et.begin(); // 트랜잭션을 시작합니다.
try { // DB 작업을 수행합니다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
// commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
} catch (Exception ex) {
ex.printStackTrace();
et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
} finally {
em.close(); // 사용한 EntityManager 를 종료합니다.
}
emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}
위 코드는 jpa환경에서 트랜잭션을 시작하는 예시이며 EntityTreansaction의 getTransaction을 사용해 트랜잭션을 가져오고
begin메서드를 통해 트랜잭션을 시작한다.
트랜잭션을 사용하기때문에 try-catch문 블록을 사용하여 오류 발생시 롤백을 수행해줘야하며, 트랜잭션이 커밋되지않으면 어떠한 변경사항도 반영되지 않는다.
사용한 entity매니저와 entitiymanagerFactory를 종료해준다 pk로 entity를 구분한다고 했는데 identifier가 그 식별자라고 이해하면된다.
Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
// memo.setId(1L); // 식별자 값을 넣어줍니다.
memo.setUsername("Robbie");
memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.
et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
영속성 컨텍스트의 기능 영속성 컨텍스트의 주요 기능에는 캐시저장소(1차 캐시), 쓰기 지연 저장소, 변경 감지 이 세가지가 있다
캐시 저장소 영속성 컨텍스트는 1차 캐시를 통해 엔티티 객체를 관리한다, 엔티티가 영속성 컨텍스트에 저장되면, 이 객체는 1차 캐시에 저장되게 된다. 캐시 저장소는 Map 자료구조 형태로 되어있으며 key는 @Id 어노테이션으로 매핑한 기본키를 저장하며 value는 Entity객체가 들어있다.
키는 #1 처럼 되어있고 value는 객체가 들어있는것을 볼 수 있다.
조회 find()를 사용해서 조회 할 수 있다. 캐시 저장소에 id가 존재하지 않을경우 출처 : 내일배움캠프 스프링 기초강의 find 메서드를 통해 엔티티를 조회할때는 먼저 1차 캐시에서 조회를 한다, 만약 엔티티가 존재하지 않는다면 영속성 컨텍스트는 select쿼리를 날려 엔티티를 조회한다. 조회된 엔티티는 1차캐시에 저장되며, 이후 find를 통해 똑같은 엔티티를 찾아도 1차캐시만 조회하게 된다.
Hibernate:
select
m1_0.id,
m1_0.contents,
m1_0.username
from
memo m1_0
where
m1_0.id=?
memo.getId() = 1
memo.getUsername() = Robbie
memo.getContents() = 영속성 컨텍스트와 트랜잭션 이해하기
SELECT 쿼리를 날린것을 볼 수 있다.
캐시 저장소에 ID가 존재할 경우 출처 : 내일배움캠프 스프링 기초강의 조회를 수행할때 이미 캐시에 존재할경우는 SELECT쿼리 없이 바로 전달해준다
이론상 자바의 관점에서는 주소값을 가지기때문에 equals 메서드를 사용한것이 아니라면 위는 false가 나와야겠지만, jpa는 객체 동일성을 보장하기때문에 같이 1을 조회한 memo1객체와 memo2객체는 서로 같다.
memo1 객체가 한번. memo객체가 한번 조회를 한다 memo2 객체는 이미 memo1객체가 id1을 조회했기때문에 캐시저장소에 등록이 되었다.
쓰기 지연 저장소 JPA도 트랜잭션처럼 쓰기 지연 저장소를 만들어 SQL에 한번에 모아서 commit후 DB에 저장한다 actionQueue가 쓰기 지연 저장소이다. 객체 두개를를 persist를 했을경우에, 쓰기지연저장소에는 2개가 등록되어있다.
flush() 쓰기지연 저장소의 내용을 db에 반영하는 역할을 수행한다.
commit을 수행하지않더라도 flush를 사용하면 등록 할 수 있다.
변경 감지 (dirty checking) jpa에서는 업데이트를 어떻게 수행할까? 출처 : 내일배움캠프 스프링 기초강의
jpa는 영속성 컨텍스트에 entity를 저장할 때 최초 상태 - 스냅샷(LoadedState)를 저장한다