@Controller
class OwnerController {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";
private final OwnerRepository owners;
private VisitRepository visits;
// Spring 4.3부터 생성자가 하나뿐이고, 생성자로 주입받는 레퍼런스 변수들이 Bean으로 등록되어 있다면 @Autowired없이 자동으로 주입을 해줌
public OwnerController(OwnerRepository clinicService, VisitRepository visits) {
this.owners = clinicService;
this.visits = visits;
}
}
여기서 OwnerController는 생성자가 하나이고, 인스턴스를 만들 때 OwnerRepository
와 VisitRepository
를 주입해주어야한다. 이때 주입은 스프링이 관리하는 객체인 Bean으로 등록이 되면 Ioc가 해당 타입의 Bean을 가져와서 주입을 함
Spring이 Bean이라는 객체를 관리해 줌 Bean들의 의존성을 관리(주입)
OwnerRepository
, VisitRepository
) 없이는 인스턴스(OwnerController
)를 만들지 못하도록 강제public class Store {
Payment payment;
public Store(Payment payment) {
this.payment = payment;
}
public void buySomething(int amount){
payment.pay(amount);
}
}
public class Cash implements Payment {
@Override
public void pay(int amount) {
System.out.println(amount + "현금 결제");
}
}
import org.springframework.util.StopWatch;
public class CashPerf implements Payment {
Payment cash = new Cash();
// 코드는 추가되었지만 기존의 코드를 건들지 않는게 중요!
@Override
public void pay(int amount) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
cash.pay(amount);
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
public interface Payment {
void pay(int amount);
}
import org.junit.jupiter.api.Test;
public class StoreTest {
@Test
public void testPay(){
Payment cashPerf = new CashPerf();
Store store = new Store(cashPerf);
store.buySomething(200);
}
}
@Around
라는 annotation을 사용하는 메서드 안에서 joinpoint
라는 파라미터를 받을 수 있는데 여기서 joinpoint는 실제 @LogExecutionTime
이 달린 Target메서드를 말한다Object proceed = joinPoint.proceed();
로 실행하고, 앞뒤로 붙이고 싶은 코드를 붙인다. return proceed;
로 결과를 반환한다.@Controller
class OwnerController {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";
private final OwnerRepository owners;
private VisitRepository visits;
public OwnerController(OwnerRepository clinicService, VisitRepository visits) {
this.owners = clinicService;
this.visits = visits;
}
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id");
}
@LogExecutionTime
@GetMapping("/owners/new")
public String initCreationForm(Map<String, Object> model) {
// 빈 객체를 만들어서 넣어주는 이유는 Form Backing Object라고해서 처음 폼을 화면에 보여줄 때 그 비어있어 보이는 폼을 채워야 하는 객체가 필요
Owner owner = new Owner();
model.put("owner", owner);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@Target(ElementType.METHOD) // 어디에 쓸 건지
@Retention(RetentionPolicy.RUNTIME) // 언제까지 쓸 건지
// 어디에 적용할지 표시해 주는 용도
public @interface LogExecutionTime {
}
@Component // Bean등록을 위해
@Aspect
// 실제 Aspect (@LogExecutionTime 이 달린 곳에 적용)
public class LogAspect {
Logger logger = (Logger) LoggerFactory.getLogger(LogAspect.class);
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable{
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object proceed = joinPoint.proceed();
stopWatch.stop();
logger.info(stopWatch.prettyPrint());
return proceed;
}
}
JSP
HTML을 코딩하기 너무 어렵고 불편해서 HTML 내부에 Java코드를 삽입하는 형식이 JSP이다. 다시 말해 서블릿의 단점을 보완하고자 만든 서블릿 기반의 스크립트 기술이다. 서블릿을 이용하게 되면 웹프로그래밍을 할 수 있지만 자바에 대한 지식이 필요하며 화면 인터페이스 구현에 너무 많은 코드를 필요로 하는 등 비효율적인 측면들이 있다. 때문에 서블릿을 작성하지 않고도 간편하게 웹프로그래밍을 구현하게 만든 기술이 JSP(Java Server Pages)
보여지는 부분은 HTML이 중심이 되는 JSP, 다른 자바클래스에게 데이터를 넘겨주는 부분은 Java코드가 중심이 되는 Servlet이 담당
Servlet은 프로그램 로직이 수행되기 유리하고, (자바 클래스니까..) JSP는 결과를 출력하기에 Servlet보다 유리하다. 필요한 html문을 그냥 입력하면 된다. 이런 장단점을 해결하기 위해,
Servlet에서 프로그램 로직이 수행되고, 그 결과를 JSP에게 포워딩하는 방법을 사용하는 것을 추천한다.
참고) Servlet Life Cycle
@GetMapping이나 @PostMapping같은 것들도 PSA인데 기본적으로는 Servlet기반으로 doGet()이나 doPost()으로 동작하지만 추상화된 인터페이스를 잘 만듦으로써 더 편하게 코드를 짤 수 있다.
@Controller, @RequestMappling 등 SpringMVC코드를 포함해 대부분의 스프링이 제공하는 API들이 PSA
SpringMVC덕분에 servlet application을 간편하게 만들 수 있다.
PSA의 'P'는 Portable로 기술에서 다른 기술로 바꿀 수 있다는 의미인데 예를 들어 Tomcat에서 Netty기반으로 의존성만 바꿔주면 동작하게 할 수도 있다. 즉, Spring이 제공해주는 추상화된 인터페이스는 그대로 쓸 수 있다.
All or nothing속성으로 모든게 제대로 이루어져야 처리를한다.
가령 카드로 물건을 구매하였을때, 계좌에서 돈이 빠져나가고, 물건이 주문이 되어야한다. 이런 과정에서 하나라도 오류가 나면 안되기 때문에 끝까지 정상적으로 이루어 졌을때 db에 commit을 하게 된다. 중간에 에러가 나면 rollback을 시킴
중간에 어떠한 insert가 실행이 되어도 commit이 안되기 때문에 적용되지않음
따라서 @Transactional 어노테이션만 달게 되면 아래와 같은 작업을 하지 않아도 된다.
TransactionExample.java
package com.mkyong.jdbc;
import java.math.BigDecimal;
import java.sql.*;
import java.time.LocalDateTime;
public class TransactionExample {
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(
"jdbc:postgresql://127.0.0.1:5432/test", "postgres", "password");
Statement statement = conn.createStatement();
PreparedStatement psInsert = conn.prepareStatement(SQL_INSERT);
PreparedStatement psUpdate = conn.prepareStatement(SQL_UPDATE)) {
statement.execute(SQL_TABLE_DROP);
statement.execute(SQL_TABLE_CREATE);
// Run list of insert commands
psInsert.setString(1, "mkyong");
psInsert.setBigDecimal(2, new BigDecimal(10));
psInsert.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
psInsert.execute();
psInsert.setString(1, "kungfu");
psInsert.setBigDecimal(2, new BigDecimal(20));
psInsert.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
psInsert.execute();
// Run list of update commands
// below line caused error, test transaction
// org.postgresql.util.PSQLException: No value specified for parameter 1.
psUpdate.setBigDecimal(2, new BigDecimal(999.99));
//psUpdate.setBigDecimal(1, new BigDecimal(999.99));
psUpdate.setString(2, "mkyong");
psUpdate.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
private static final String SQL_INSERT = "INSERT INTO EMPLOYEE (NAME, SALARY, CREATED_DATE) VALUES (?,?,?)";
private static final String SQL_UPDATE = "UPDATE EMPLOYEE SET SALARY=? WHERE NAME=?";
private static final String SQL_TABLE_CREATE = "CREATE TABLE EMPLOYEE"
+ "("
+ " ID serial,"
+ " NAME varchar(100) NOT NULL,"
+ " SALARY numeric(15, 2) NOT NULL,"
+ " CREATED_DATE timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+ " PRIMARY KEY (ID)"
+ ")";
private static final String SQL_TABLE_DROP = "DROP TABLE EMPLOYEE";
}
@Transactional도 역시 PSA이고 덕분에 사용하는 db에 따라 JpaTransactionManager, DatasourceTransactionManager, HibernateTransactionManager 을 코드 변경없이 사용할 수 있다.
Transactional 어노테이션을 처리하는 aspect에서는 Transaction처리를 기술에 독립적인 Platform TransactionManager라는 인터페이스를 사용해서 구현되어 있다. 그래서 JpaTransactionManager, DatasourceTransactionManager, HibernateTransactionManager 같은 구현체들 bean이 바뀌더라도 transaction을 처리하는 aspect코드들은 바뀌지 않는다.
스프링 트랜잭션 추상화의 핵심 인터페이스는 PlatformTransactionManager 모든 스프링의 트랜잭션 기능과 코드는 이 인터페이스를 통해서 로우레벨의 트랜잭션 서비스를 이용할 수 있다.
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(Transaction status) throws TransactionException;
}
기존에 MyBatis같은 SQL Mapper를 이용해 쿼리를 작성하면 개발하는 시간보다 SQL을 다루는 시간이 더 많이 들어가서 개체지향 프로그래밍을 하는데 비효율적이다. 따라서 JPA ORM을 사용함으로써 개체지향 프로그래밍을 하는데 집중할 수 있다. 참고로 MyBatis는 SQL Mapper지 ORM은 아니다. ORM은 객체를 매핑하고, SQL Mapper는 쿼리를 매핑한다.
interface인 JPA를 사용하기 위해서는 구현체가 필요하다. 대표적으로 Hibernate가 있지만 Spring에서는 구현체를 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA(권장)가 있다. (JPA <- Hibernate <- Spring Data JPA)
Hibernate와 Spring Data JPA를 쓰는 것이 큰 차이가 없음에도 Spring Data JPA를 권장하는 이유
또 특이한 점은 JPA의 영속성 컨텍스트 덕분에 쿼리문이 없다. 영속성 컨텍스트란 Entity를 영구 저장하는 환경이다. 트랜잭션안에서 DB에서 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태이다. 이 상태에서 해당 데이터의 값을 변경하면 트랜잭션이 끝나느 시점에 해당 테이블에 변경분을 반영하게 된다. 즉 Entity의 값만 변경하면 된다.
도메인 패키지에서 Entity클래스를 만들 때 절대 Setter 메서드를 만들지 않는다. 무작정 만들면 언제 어디서 변해야하는지 코드상으로 알기 어렵다. 이럴때는 알기 쉽게 메서드를 추가한다.
그런데 Setter가 없는데 어떻게 값을 채워 DB에 삽입할까? 기본적인 구조는 생성자(빌더로 해도 됨)를 통해 최종값을 채우고 , DB에 Insert하는 것이고, 값 변경이 필요한 경우 Setter대신 만든 메서드를 호출하여 변경하게 된다.
참고로 빌더를 사용하면 어느 필드에 어떤 값을 채워야할지 명확하게 알 수 있다.
Entity클래스와 Repository 클래스(My batis에서 Dao라고 불리는 DB Layer)는 기본적으로 함께 움직여야한다. 특히 Repository는 @Repository도 붙일 필요도 없고, 아무 annotation이 없어도, JpaRepository<Entity class, PK type>을 상속하면 IOC컨테이너에서 관리하는 Bean에 주입된다.
Entity클래스와 유사하지만 별도로 분리한 이유는 Entity 클래스를 Request/Response 클래스로 사용해서는 안되기 때문이다. Entity클래스는 DB와 맞닿아 있는 핵심 클래스이기 때문이다. 즉, 수많은 클래스나 비즈니스 로직들이 Entity클래스를 기준으로 동작한다. Request와 Response용 Dto는 View를 위한 클래스이기 때문에 자주 변경이 필요하다. 이런식으로 View Layer와 DB Layer의 역할을 철저하게 분리하는게 좋다.
또, Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우가 많은데 단순히 Entity클래스로만 표현하기가 어렵기 때문에 분리해서 사용한다. 결론적으로 Service와 Controller에서 사용한다.
Entity에는 유지보수를 위해 데이터의 생성시간과 수정시간을 보통 포함하는데 매번 생성해서 삽입할 수는 없는 꼴이다. 그래서 Java8부터 제공하는 LocalDate/LocalDateTime을 사용한다. 데이터베이스에 제대로 매핑되지 않는 이슈가 있었지만 Hibernate 5.2.10버전이상 springboot 2.x 버전을 사용하면 된다.
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass // JPA Entity Class들이 BaseTimeEntity을 상속할 경우 필드들(createdDate,modifiedDate)도 column으로 인식하게 함, 앞으로 추가될 Entitiy들은 상속만 받으면 등록일/수정일 고민할 필요 X
@EntityListeners(AuditingEntityListener.class) // class에 Auditing기능 포함시킴
public class BaseTimeEntity {
@CreatedDate // Entity가 생성되어 저장될 때 자동 저장
private LocalDateTime createdDate;
@LastModifiedDate // 조회한 Entity의 값을 변경할 때 시간이 자동 저장
private LocalDateTime modifiedDate;
}
그리고 프로젝트가 시작되는 최상위 클래스에 @EnableJpaAuditing
을 달아서 JPA Auditing을 활성화 시켜준다.