├── mvc ├── src │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── interface21 │ │ │ └── .gitkeep │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── jakarta.servlet.ServletContainerInitializer │ │ └── java │ │ └── com │ │ └── interface21 │ │ ├── web │ │ ├── bind │ │ │ └── annotation │ │ │ │ ├── RequestMethod.java │ │ │ │ ├── PathVariable.java │ │ │ │ ├── RequestParam.java │ │ │ │ └── RequestMapping.java │ │ ├── http │ │ │ └── MediaType.java │ │ ├── WebApplicationInitializer.java │ │ └── SpringServletContainerInitializer.java │ │ ├── webmvc │ │ └── servlet │ │ │ ├── mvc │ │ │ ├── HandlerMapping.java │ │ │ ├── asis │ │ │ │ ├── Controller.java │ │ │ │ ├── ForwardController.java │ │ │ │ └── ControllerHandlerAdapter.java │ │ │ ├── HandlerAdapter.java │ │ │ ├── HandlerAdapterRegistry.java │ │ │ ├── tobe │ │ │ │ ├── HandlerExecutionHandlerAdapter.java │ │ │ │ ├── HandlerKey.java │ │ │ │ ├── HandlerExecution.java │ │ │ │ ├── ControllerScanner.java │ │ │ │ └── AnnotationHandlerMapping.java │ │ │ ├── HandlerExecutor.java │ │ │ ├── HandlerMappingRegistry.java │ │ │ └── DispatcherServlet.java │ │ │ ├── View.java │ │ │ ├── ModelAndView.java │ │ │ └── view │ │ │ ├── JspView.java │ │ │ └── JsonView.java │ │ ├── context │ │ └── stereotype │ │ │ └── Controller.java │ │ └── core │ │ └── util │ │ └── ReflectionUtils.java └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── jdbc ├── src │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── interface21 │ │ │ └── jdbc │ │ │ └── core │ │ │ └── JdbcTemplateTest.java │ └── main │ │ └── java │ │ └── com │ │ └── interface21 │ │ ├── jdbc │ │ ├── core │ │ │ └── JdbcTemplate.java │ │ ├── CannotGetJdbcConnectionException.java │ │ └── datasource │ │ │ └── DataSourceUtils.java │ │ ├── transaction │ │ └── support │ │ │ └── TransactionSynchronizationManager.java │ │ └── dao │ │ └── DataAccessException.java └── build.gradle ├── .gitattributes ├── study ├── src │ ├── test │ │ └── java │ │ │ ├── aop │ │ │ ├── stage2 │ │ │ │ ├── AopConfig.java │ │ │ │ ├── UserService.java │ │ │ │ └── Stage2Test.java │ │ │ ├── stage1 │ │ │ │ ├── TransactionAdvice.java │ │ │ │ ├── TransactionPointcut.java │ │ │ │ ├── TransactionAdvisor.java │ │ │ │ ├── UserService.java │ │ │ │ └── Stage1Test.java │ │ │ ├── stage0 │ │ │ │ ├── TransactionHandler.java │ │ │ │ └── Stage0Test.java │ │ │ └── StubUserHistoryDao.java │ │ │ ├── transaction │ │ │ ├── stage2 │ │ │ │ ├── UserRepository.java │ │ │ │ ├── User.java │ │ │ │ ├── SecondUserService.java │ │ │ │ ├── Stage2Test.java │ │ │ │ └── FirstUserService.java │ │ │ └── stage1 │ │ │ │ ├── jdbc │ │ │ │ ├── RowMapper.java │ │ │ │ ├── PreparedStatementSetter.java │ │ │ │ ├── PreparedStatementCreator.java │ │ │ │ ├── KeyHolder.java │ │ │ │ ├── DataAccessException.java │ │ │ │ └── JdbcTemplate.java │ │ │ │ ├── User.java │ │ │ │ ├── UserDao.java │ │ │ │ └── Stage1Test.java │ │ │ └── connectionpool │ │ │ ├── stage0 │ │ │ └── Stage0Test.java │ │ │ ├── stage2 │ │ │ └── Stage2Test.java │ │ │ ├── stage1 │ │ │ └── Stage1Test.java │ │ │ └── PoolingVsNoPoolingTest.java │ └── main │ │ ├── java │ │ ├── transaction │ │ │ ├── ThrowingRunnable.java │ │ │ ├── ThrowingConsumer.java │ │ │ ├── ThrowingFunction.java │ │ │ ├── App.java │ │ │ ├── RunnableWrapper.java │ │ │ ├── ConsumerWrapper.java │ │ │ ├── FunctionWrapper.java │ │ │ └── DatabasePopulatorUtils.java │ │ ├── aop │ │ │ ├── Transactional.java │ │ │ ├── service │ │ │ │ ├── UserService.java │ │ │ │ ├── AppUserService.java │ │ │ │ └── TxUserService.java │ │ │ ├── App.java │ │ │ ├── config │ │ │ │ └── DataSourceConfig.java │ │ │ ├── DataAccessException.java │ │ │ ├── repository │ │ │ │ ├── UserHistoryDao.java │ │ │ │ └── UserDao.java │ │ │ └── domain │ │ │ │ ├── User.java │ │ │ │ └── UserHistory.java │ │ └── connectionpool │ │ │ ├── App.java │ │ │ └── DataSourceConfig.java │ │ └── resources │ │ ├── application.yml │ │ └── schema.sql └── build.gradle ├── app ├── src │ ├── main │ │ ├── webapp │ │ │ ├── include │ │ │ │ ├── footer.jspf │ │ │ │ └── header.jspf │ │ │ ├── assets │ │ │ │ ├── chart-pie.js │ │ │ │ ├── chart-bar.js │ │ │ │ ├── chart-area.js │ │ │ │ └── img │ │ │ │ │ └── error-404-monochrome.svg │ │ │ ├── js │ │ │ │ └── scripts.js │ │ │ ├── 500.jsp │ │ │ ├── 404.jsp │ │ │ ├── 401.jsp │ │ │ ├── login.jsp │ │ │ ├── register.jsp │ │ │ ├── index.jsp │ │ │ └── index.html │ │ ├── resources │ │ │ ├── logback.xml │ │ │ └── schema.sql │ │ └── java │ │ │ └── com │ │ │ └── techcourse │ │ │ ├── controller │ │ │ ├── LogoutController.java │ │ │ ├── UserSession.java │ │ │ ├── UserController.java │ │ │ ├── RegisterController.java │ │ │ └── LoginController.java │ │ │ ├── support │ │ │ ├── context │ │ │ │ └── ContextLoaderListener.java │ │ │ ├── web │ │ │ │ └── filter │ │ │ │ │ ├── CharacterEncodingFilter.java │ │ │ │ │ └── ResourceFilter.java │ │ │ └── jdbc │ │ │ │ └── init │ │ │ │ └── DatabasePopulatorUtils.java │ │ │ ├── config │ │ │ └── DataSourceConfig.java │ │ │ ├── repository │ │ │ └── InMemoryUserRepository.java │ │ │ ├── service │ │ │ └── UserService.java │ │ │ ├── ManualHandlerMapping.java │ │ │ ├── Application.java │ │ │ ├── domain │ │ │ ├── User.java │ │ │ └── UserHistory.java │ │ │ ├── AppWebApplicationInitializer.java │ │ │ └── dao │ │ │ ├── UserHistoryDao.java │ │ │ └── UserDao.java │ └── test │ │ └── java │ │ └── com │ │ └── techcourse │ │ ├── service │ │ ├── MockUserHistoryDao.java │ │ └── UserServiceTest.java │ │ └── dao │ │ └── UserDaoTest.java └── build.gradle ├── README.md ├── gradlew.bat ├── .gitignore └── gradlew /mvc/src/test/java/com/interface21/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/java-jdbc/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'java-jdbc' 2 | include 'jdbc' 3 | include 'mvc' 4 | include 'app' 5 | include 'study' 6 | -------------------------------------------------------------------------------- /mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer: -------------------------------------------------------------------------------- 1 | com.interface21.web.SpringServletContainerInitializer 2 | -------------------------------------------------------------------------------- /jdbc/src/test/java/com/interface21/jdbc/core/JdbcTemplateTest.java: -------------------------------------------------------------------------------- 1 | package com.interface21.jdbc.core; 2 | 3 | class JdbcTemplateTest { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage2/AopConfig.java: -------------------------------------------------------------------------------- 1 | package aop.stage2; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | 5 | @Configuration 6 | public class AopConfig { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/webapp/include/footer.jspf: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/ThrowingRunnable.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingRunnable { 5 | void run() throws E; 6 | } 7 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/ThrowingConsumer.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingConsumer { 5 | void accept(T t) throws E; 6 | } 7 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/ThrowingFunction.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | @FunctionalInterface 4 | public interface ThrowingFunction { 5 | R apply(T t) throws E; 6 | } 7 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web.bind.annotation; 2 | 3 | public enum RequestMethod { 4 | GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE 5 | } 6 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/http/MediaType.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web.http; 2 | 3 | public class MediaType { 4 | public static final String APPLICATION_JSON_UTF8_VALUE = "application/json;charset=UTF-8"; 5 | } 6 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage2/UserRepository.java: -------------------------------------------------------------------------------- 1 | package transaction.stage2; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface UserRepository extends JpaRepository { 6 | } 7 | -------------------------------------------------------------------------------- /study/src/main/java/aop/Transactional.java: -------------------------------------------------------------------------------- 1 | package aop; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target({ElementType.METHOD}) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Inherited 8 | @Documented 9 | public @interface Transactional { 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 29 16:48:55 KST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/jdbc/RowMapper.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1.jdbc; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | @FunctionalInterface 7 | public interface RowMapper { 8 | T mapRow(final ResultSet rs) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/jdbc/PreparedStatementSetter.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1.jdbc; 2 | 3 | import java.sql.PreparedStatement; 4 | import java.sql.SQLException; 5 | 6 | public interface PreparedStatementSetter { 7 | void setParameters(final PreparedStatement pstmt) throws SQLException; 8 | } 9 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/WebApplicationInitializer.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web; 2 | 3 | import jakarta.servlet.ServletContext; 4 | import jakarta.servlet.ServletException; 5 | 6 | public interface WebApplicationInitializer { 7 | void onStartup(ServletContext servletContext) throws ServletException; 8 | } 9 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerMapping.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | 5 | public interface HandlerMapping { 6 | 7 | void initialize(); 8 | 9 | Object getHandler(final HttpServletRequest request); 10 | } 11 | -------------------------------------------------------------------------------- /study/src/main/java/aop/service/UserService.java: -------------------------------------------------------------------------------- 1 | package aop.service; 2 | 3 | 4 | import aop.domain.User; 5 | 6 | public interface UserService { 7 | 8 | User findById(final long id); 9 | void insert(final User user); 10 | 11 | void changePassword(final long id, final String newPassword, final String createBy); 12 | } 13 | -------------------------------------------------------------------------------- /study/src/main/java/aop/App.java: -------------------------------------------------------------------------------- 1 | package aop; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class App { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(App.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.asis; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | 6 | public interface Controller { 7 | String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception; 8 | } 9 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/jdbc/PreparedStatementCreator.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1.jdbc; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.SQLException; 6 | 7 | public interface PreparedStatementCreator { 8 | PreparedStatement createPreparedStatement(final Connection con) throws SQLException; 9 | } 10 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/App.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class App { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(App.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /study/src/main/java/connectionpool/App.java: -------------------------------------------------------------------------------- 1 | package connectionpool; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class App { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(App.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/View.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | 6 | import java.util.Map; 7 | 8 | public interface View { 9 | void render(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/bind/annotation/PathVariable.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web.bind.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.PARAMETER) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Documented 8 | public @interface PathVariable { 9 | String value() default ""; 10 | 11 | String name() default ""; 12 | 13 | boolean required() default true; 14 | } 15 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/bind/annotation/RequestParam.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web.bind.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.PARAMETER) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Documented 8 | public @interface RequestParam { 9 | String value() default ""; 10 | 11 | String name() default ""; 12 | 13 | boolean required() default true; 14 | } 15 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage1/TransactionAdvice.java: -------------------------------------------------------------------------------- 1 | package aop.stage1; 2 | 3 | import org.aopalliance.intercept.MethodInterceptor; 4 | import org.aopalliance.intercept.MethodInvocation; 5 | 6 | /** 7 | * 어드바이스(advice). 부가기능을 담고 있는 클래스 8 | */ 9 | public class TransactionAdvice implements MethodInterceptor { 10 | 11 | @Override 12 | public Object invoke(final MethodInvocation invocation) throws Throwable { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/jdbc/KeyHolder.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1.jdbc; 2 | 3 | public class KeyHolder { 4 | 5 | private long id; 6 | 7 | public void setId(long id) { 8 | this.id = id; 9 | } 10 | 11 | public long getId() { 12 | return id; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return "KeyHolder{" + 18 | "id=" + id + 19 | '}'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 만들면서 배우는 스프링 2 | 3 | ## JDBC 라이브러리 구현하기 4 | 5 | ### 학습목표 6 | - JDBC 라이브러리를 구현하는 경험을 함으로써 중복을 제거하는 연습을 한다. 7 | - Transaction 적용을 위해 알아야할 개념을 이해한다. 8 | 9 | ### 시작 가이드 10 | 1. 이전 미션에서 진행한 코드를 사용하고 싶다면, 마이그레이션 작업을 진행합니다. 11 | - 학습 테스트는 강의 시간에 풀어봅시다. 12 | 2. LMS의 1단계 미션부터 진행합니다. 13 | 14 | ## 준비 사항 15 | - 강의 시작 전에 docker를 설치해주세요. 16 | 17 | ## 학습 테스트 18 | 1. [ConnectionPool](study/src/test/java/connectionpool) 19 | 2. [Transaction](study/src/test/java/transaction) 20 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/context/stereotype/Controller.java: -------------------------------------------------------------------------------- 1 | package com.interface21.context.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.TYPE}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Controller { 11 | String value() default ""; 12 | 13 | String path() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage0/TransactionHandler.java: -------------------------------------------------------------------------------- 1 | package aop.stage0; 2 | 3 | import java.lang.reflect.InvocationHandler; 4 | import java.lang.reflect.Method; 5 | 6 | public class TransactionHandler implements InvocationHandler { 7 | 8 | /** 9 | * @Transactional 어노테이션이 존재하는 메서드만 트랜잭션 기능을 적용하도록 만들어보자. 10 | */ 11 | @Override 12 | public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jdbc/src/main/java/com/interface21/jdbc/core/JdbcTemplate.java: -------------------------------------------------------------------------------- 1 | package com.interface21.jdbc.core; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.sql.DataSource; 7 | 8 | public class JdbcTemplate { 9 | 10 | private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); 11 | 12 | private final DataSource dataSource; 13 | 14 | public JdbcTemplate(final DataSource dataSource) { 15 | this.dataSource = dataSource; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/bind/annotation/RequestMapping.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web.bind.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.METHOD, ElementType.TYPE}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface RequestMapping { 11 | String value() default ""; 12 | 13 | RequestMethod[] method() default {}; 14 | } 15 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc; 2 | 3 | import com.interface21.webmvc.servlet.ModelAndView; 4 | 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | 8 | public interface HandlerAdapter { 9 | boolean supports(final Object handler); 10 | 11 | ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/webapp/include/header.jspf: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | 6 | 7 | 대시보드 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%-5level] [%thread] [%logger{36}] - %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /jdbc/src/main/java/com/interface21/jdbc/CannotGetJdbcConnectionException.java: -------------------------------------------------------------------------------- 1 | package com.interface21.jdbc; 2 | 3 | import java.sql.SQLException; 4 | 5 | public class CannotGetJdbcConnectionException extends RuntimeException { 6 | 7 | public CannotGetJdbcConnectionException(String msg) { 8 | super(msg); 9 | } 10 | 11 | public CannotGetJdbcConnectionException(String msg, SQLException ex) { 12 | super(msg, ex); 13 | } 14 | 15 | public CannotGetJdbcConnectionException(String msg, IllegalStateException ex) { 16 | super(msg, ex); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists users ( 2 | id bigint auto_increment, 3 | account varchar(100) not null, 4 | password varchar(100) not null, 5 | email varchar(100) not null, 6 | primary key(id) 7 | ); 8 | 9 | create table if not exists user_history ( 10 | id bigint auto_increment, 11 | user_id bigint not null, 12 | account varchar(100) not null, 13 | password varchar(100) not null, 14 | email varchar(100) not null, 15 | created_at datetime not null, 16 | created_by varchar(100) not null, 17 | primary key(id) 18 | ); 19 | -------------------------------------------------------------------------------- /study/src/test/java/aop/StubUserHistoryDao.java: -------------------------------------------------------------------------------- 1 | package aop; 2 | 3 | import aop.domain.UserHistory; 4 | import aop.repository.UserHistoryDao; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public class StubUserHistoryDao extends UserHistoryDao { 10 | 11 | public StubUserHistoryDao(final JdbcTemplate jdbcTemplate) { 12 | super(jdbcTemplate); 13 | } 14 | 15 | @Override 16 | public void log(final UserHistory userHistory) { 17 | throw new DataAccessException(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage1/TransactionPointcut.java: -------------------------------------------------------------------------------- 1 | package aop.stage1; 2 | 3 | import org.springframework.aop.support.StaticMethodMatcherPointcut; 4 | 5 | import java.lang.reflect.Method; 6 | 7 | /** 8 | * 포인트컷(pointcut). 어드바이스를 적용할 조인 포인트를 선별하는 클래스. 9 | * TransactionPointcut 클래스는 메서드를 대상으로 조인 포인트를 찾는다. 10 | * 11 | * 조인 포인트(join point). 어드바이스가 적용될 위치 12 | */ 13 | public class TransactionPointcut extends StaticMethodMatcherPointcut { 14 | 15 | @Override 16 | public boolean matches(final Method method, final Class> targetClass) { 17 | return false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /study/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | open-in-view: false 4 | show-sql: true 5 | generate-ddl: true 6 | hibernate: 7 | ddl-auto: create-drop # 주의! 로컬 테스트에서만 사용한다. 8 | datasource: 9 | hikari: 10 | jdbc-url: jdbc:h2:./test;DB_CLOSE_DELAY=-1;MODE=MYSQL; # TRACE_LEVEL_SYSTEM_OUT=3; # h2에서 출력하는 트랜잭션 로그 확인할 때 사용 11 | username: sa 12 | password: 13 | 14 | # 스프링에서 출력하는 트랜잭션 로그를 직접 보고 싶으면 아래 주석 해제 15 | #logging: 16 | # level: 17 | # org.springframework.transaction.interceptor: TRACE 18 | # org.springframework.transaction.support: DEBUG 19 | -------------------------------------------------------------------------------- /app/src/test/java/com/techcourse/service/MockUserHistoryDao.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.service; 2 | 3 | import com.techcourse.dao.UserHistoryDao; 4 | import com.techcourse.domain.UserHistory; 5 | import com.interface21.dao.DataAccessException; 6 | import com.interface21.jdbc.core.JdbcTemplate; 7 | 8 | public class MockUserHistoryDao extends UserHistoryDao { 9 | 10 | public MockUserHistoryDao(final JdbcTemplate jdbcTemplate) { 11 | super(jdbcTemplate); 12 | } 13 | 14 | @Override 15 | public void log(final UserHistory userHistory) { 16 | throw new DataAccessException(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/controller/LogoutController.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.controller; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import com.interface21.webmvc.servlet.mvc.asis.Controller; 6 | 7 | public class LogoutController implements Controller { 8 | 9 | @Override 10 | public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { 11 | final var session = req.getSession(); 12 | session.removeAttribute(UserSession.SESSION_KEY); 13 | return "redirect:/"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.asis; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | 6 | import java.util.Objects; 7 | 8 | public class ForwardController implements Controller { 9 | 10 | private final String path; 11 | 12 | public ForwardController(final String path) { 13 | this.path = Objects.requireNonNull(path); 14 | } 15 | 16 | @Override 17 | public String execute(final HttpServletRequest request, final HttpServletResponse response) { 18 | return path; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/support/context/ContextLoaderListener.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.support.context; 2 | 3 | import com.techcourse.config.DataSourceConfig; 4 | import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; 5 | import jakarta.servlet.ServletContextEvent; 6 | import jakarta.servlet.ServletContextListener; 7 | import jakarta.servlet.annotation.WebListener; 8 | 9 | @WebListener 10 | public class ContextLoaderListener implements ServletContextListener { 11 | 12 | @Override 13 | public void contextInitialized(final ServletContextEvent sce) { 14 | DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/webapp/assets/chart-pie.js: -------------------------------------------------------------------------------- 1 | // Set new default font family and font color to mimic Bootstrap's default styling 2 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 3 | Chart.defaults.global.defaultFontColor = '#292b2c'; 4 | 5 | // Pie Chart Example 6 | var ctx = document.getElementById("myPieChart"); 7 | var myPieChart = new Chart(ctx, { 8 | type: 'pie', 9 | data: { 10 | labels: ["Blue", "Red", "Yellow", "Green"], 11 | datasets: [{ 12 | data: [12.21, 15.58, 11.25, 8.32], 13 | backgroundColor: ['#007bff', '#dc3545', '#ffc107', '#28a745'], 14 | }], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage1/TransactionAdvisor.java: -------------------------------------------------------------------------------- 1 | package aop.stage1; 2 | 3 | import org.aopalliance.aop.Advice; 4 | import org.springframework.aop.Pointcut; 5 | import org.springframework.aop.PointcutAdvisor; 6 | 7 | /** 8 | * 어드바이저(advisor). 포인트컷과 어드바이스를 하나씩 갖고 있는 객체. 9 | * AOP의 애스팩트(aspect)에 해당되는 클래스다. 10 | */ 11 | public class TransactionAdvisor implements PointcutAdvisor { 12 | 13 | @Override 14 | public Pointcut getPointcut() { 15 | return null; 16 | } 17 | 18 | @Override 19 | public Advice getAdvice() { 20 | return null; 21 | } 22 | 23 | @Override 24 | public boolean isPerInstance() { 25 | return false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/RunnableWrapper.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public final class RunnableWrapper { 7 | 8 | private static final Logger log = LoggerFactory.getLogger(RunnableWrapper.class); 9 | 10 | public static Runnable accept(ThrowingRunnable runnable) { 11 | return () -> { 12 | try { 13 | runnable.run(); 14 | } catch (Exception e) { 15 | log.error(e.getMessage(), e.getCause()); 16 | throw new RuntimeException(e); 17 | } 18 | }; 19 | } 20 | 21 | private RunnableWrapper() {} 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/controller/UserSession.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.controller; 2 | 3 | import com.techcourse.domain.User; 4 | import jakarta.servlet.http.HttpSession; 5 | 6 | import java.util.Optional; 7 | 8 | public class UserSession { 9 | 10 | public static final String SESSION_KEY = "user"; 11 | 12 | public static Optional getUserFrom(final HttpSession session) { 13 | final var user = (User) session.getAttribute(SESSION_KEY); 14 | return Optional.ofNullable(user); 15 | } 16 | 17 | public static boolean isLoggedIn(final HttpSession session) { 18 | return getUserFrom(session).isPresent(); 19 | } 20 | 21 | private UserSession() {} 22 | } 23 | -------------------------------------------------------------------------------- /study/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | -- mysql 8.0.30부터는 statement.execute()으로 여러 쿼리를 한 번에 실행할 수 없다. 2 | -- 멀티 쿼리 옵션을 url로 전달하도록 수정하는 방법을 찾아서 적용하자. 3 | CREATE TABLE IF NOT EXISTS users ( 4 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 5 | account VARCHAR(100) NOT NULL, 6 | password VARCHAR(100) NOT NULL, 7 | email VARCHAR(100) NOT NULL 8 | ) ENGINE=INNODB; 9 | 10 | CREATE TABLE IF NOT EXISTS user_history ( 11 | id BIGINT AUTO_INCREMENT PRIMARY KEY, 12 | user_id BIGINT NOT NULL, 13 | account VARCHAR(100) NOT NULL, 14 | password VARCHAR(100) NOT NULL, 15 | email VARCHAR(100) NOT NULL, 16 | created_at DATETIME NOT NULL, 17 | created_by VARCHAR(100) NOT NULL 18 | ) ENGINE=INNODB; 19 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerAdapterRegistry.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class HandlerAdapterRegistry { 7 | 8 | private final List handlerAdapters = new ArrayList<>(); 9 | 10 | public void addHandlerAdapter(final HandlerAdapter handlerAdapter) { 11 | handlerAdapters.add(handlerAdapter); 12 | } 13 | 14 | public HandlerAdapter getHandlerAdapter(final Object handler) { 15 | return handlerAdapters.stream() 16 | .filter(ha -> ha.supports(handler)) 17 | .findFirst() 18 | .orElseThrow(IllegalArgumentException::new); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/ConsumerWrapper.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.function.Consumer; 7 | 8 | public final class ConsumerWrapper { 9 | 10 | private static final Logger log = LoggerFactory.getLogger(ConsumerWrapper.class); 11 | 12 | public static Consumer accept(ThrowingConsumer consumer) { 13 | return i -> { 14 | try { 15 | consumer.accept(i); 16 | } catch (Exception e) { 17 | log.error(e.getMessage(), e.getCause()); 18 | throw new RuntimeException(e); 19 | } 20 | }; 21 | } 22 | 23 | private ConsumerWrapper() {} 24 | } 25 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/FunctionWrapper.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.util.function.Function; 7 | 8 | public class FunctionWrapper { 9 | 10 | private static final Logger log = LoggerFactory.getLogger(FunctionWrapper.class); 11 | 12 | public static Function apply(ThrowingFunction function) { 13 | return i -> { 14 | try { 15 | return function.apply(i); 16 | } catch (Exception e) { 17 | log.error(e.getMessage(), e.getCause()); 18 | throw new RuntimeException(e); 19 | } 20 | }; 21 | } 22 | 23 | private FunctionWrapper() {} 24 | } 25 | -------------------------------------------------------------------------------- /jdbc/src/main/java/com/interface21/transaction/support/TransactionSynchronizationManager.java: -------------------------------------------------------------------------------- 1 | package com.interface21.transaction.support; 2 | 3 | import javax.sql.DataSource; 4 | import java.sql.Connection; 5 | import java.util.Map; 6 | 7 | public abstract class TransactionSynchronizationManager { 8 | 9 | private static final ThreadLocal> resources = new ThreadLocal<>(); 10 | 11 | private TransactionSynchronizationManager() {} 12 | 13 | public static Connection getResource(DataSource key) { 14 | return null; 15 | } 16 | 17 | public static void bindResource(DataSource key, Connection value) { 18 | } 19 | 20 | public static Connection unbindResource(DataSource key) { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /jdbc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | java { 6 | sourceCompatibility = JavaVersion.VERSION_21 7 | targetCompatibility = JavaVersion.VERSION_21 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation 'org.reflections:reflections:0.10.2' 16 | implementation 'ch.qos.logback:logback-classic:1.5.18' 17 | implementation 'org.apache.commons:commons-lang3:3.18.0' 18 | 19 | testImplementation 'org.assertj:assertj-core:3.26.3' 20 | testImplementation 'org.mockito:mockito-core:5.15.2' 21 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.13.4' 22 | testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.4' 23 | } 24 | 25 | test { 26 | useJUnitPlatform() 27 | } 28 | -------------------------------------------------------------------------------- /study/src/main/java/aop/config/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package aop.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 6 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 7 | 8 | import javax.sql.DataSource; 9 | 10 | @Configuration 11 | public class DataSourceConfig { 12 | 13 | @Bean 14 | public DataSource dataSource() { 15 | return new EmbeddedDatabaseBuilder() 16 | .setType(EmbeddedDatabaseType.H2) 17 | .setName("test;DB_CLOSE_DELAY=-1;MODE=MYSQL;") 18 | .addScript("classpath:schema.sql") 19 | .build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /study/src/main/java/aop/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package aop; 2 | 3 | public class DataAccessException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public DataAccessException() { 7 | super(); 8 | } 9 | 10 | public DataAccessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 11 | super(message, cause, enableSuppression, writableStackTrace); 12 | } 13 | 14 | public DataAccessException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public DataAccessException(String message) { 19 | super(message); 20 | } 21 | 22 | public DataAccessException(Throwable cause) { 23 | super(cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /jdbc/src/main/java/com/interface21/dao/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package com.interface21.dao; 2 | 3 | public class DataAccessException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public DataAccessException() { 8 | super(); 9 | } 10 | 11 | public DataAccessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 12 | super(message, cause, enableSuppression, writableStackTrace); 13 | } 14 | 15 | public DataAccessException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public DataAccessException(String message) { 20 | super(message); 21 | } 22 | 23 | public DataAccessException(Throwable cause) { 24 | super(cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/jdbc/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1.jdbc; 2 | 3 | public class DataAccessException extends RuntimeException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public DataAccessException() { 7 | super(); 8 | } 9 | 10 | public DataAccessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 11 | super(message, cause, enableSuppression, writableStackTrace); 12 | } 13 | 14 | public DataAccessException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public DataAccessException(String message) { 19 | super(message); 20 | } 21 | 22 | public DataAccessException(Throwable cause) { 23 | super(cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionHandlerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.tobe; 2 | 3 | import com.interface21.webmvc.servlet.ModelAndView; 4 | import com.interface21.webmvc.servlet.mvc.HandlerAdapter; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | 8 | public class HandlerExecutionHandlerAdapter implements HandlerAdapter { 9 | 10 | @Override 11 | public boolean supports(final Object handler) { 12 | return handler instanceof HandlerExecution; 13 | } 14 | 15 | @Override 16 | public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { 17 | return ((HandlerExecution) handler).handle(request, response); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/config/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.config; 2 | 3 | import org.h2.jdbcx.JdbcDataSource; 4 | 5 | import java.util.Objects; 6 | 7 | public class DataSourceConfig { 8 | 9 | private static javax.sql.DataSource INSTANCE; 10 | 11 | public static javax.sql.DataSource getInstance() { 12 | if (Objects.isNull(INSTANCE)) { 13 | INSTANCE = createJdbcDataSource(); 14 | } 15 | return INSTANCE; 16 | } 17 | 18 | private static JdbcDataSource createJdbcDataSource() { 19 | final var jdbcDataSource = new JdbcDataSource(); 20 | jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;"); 21 | jdbcDataSource.setUser(""); 22 | jdbcDataSource.setPassword(""); 23 | return jdbcDataSource; 24 | } 25 | 26 | private DataSourceConfig() {} 27 | } 28 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerExecutor.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import com.interface21.webmvc.servlet.ModelAndView; 6 | 7 | public class HandlerExecutor { 8 | 9 | private final HandlerAdapterRegistry handlerAdapterRegistry; 10 | 11 | public HandlerExecutor(final HandlerAdapterRegistry handlerAdapterRegistry) { 12 | this.handlerAdapterRegistry = handlerAdapterRegistry; 13 | } 14 | 15 | public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { 16 | final var handlerAdapter = handlerAdapterRegistry.getHandlerAdapter(handler); 17 | return handlerAdapter.handle(request, response, handler); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerMappingRegistry.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | 9 | public class HandlerMappingRegistry { 10 | 11 | private final List handlerMappings = new ArrayList<>(); 12 | 13 | public void addHandlerMapping(final HandlerMapping handlerMapping) { 14 | handlerMapping.initialize(); 15 | handlerMappings.add(handlerMapping); 16 | } 17 | 18 | public Optional getHandler(final HttpServletRequest request) { 19 | return handlerMappings.stream() 20 | .map(hm -> hm.getHandler(request)) 21 | .filter(Objects::nonNull) 22 | .findFirst(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/repository/InMemoryUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.repository; 2 | 3 | import com.techcourse.domain.User; 4 | 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | public class InMemoryUserRepository { 10 | 11 | private static final Map database = new ConcurrentHashMap<>(); 12 | 13 | static { 14 | final User user = new User(1, "gugu", "password", "hkkang@woowahan.com"); 15 | database.put(user.getAccount(), user); 16 | } 17 | 18 | public static void save(final User user) { 19 | database.put(user.getAccount(), user); 20 | } 21 | 22 | public static Optional findByAccount(final String account) { 23 | return Optional.ofNullable(database.get(account)); 24 | } 25 | 26 | private InMemoryUserRepository() {} 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/support/web/filter/CharacterEncodingFilter.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.support.web.filter; 2 | 3 | import jakarta.servlet.*; 4 | import jakarta.servlet.annotation.WebFilter; 5 | 6 | import java.io.IOException; 7 | 8 | @WebFilter("/*") 9 | public class CharacterEncodingFilter implements Filter { 10 | 11 | private static final String DEFAULT_ENCODING = "UTF-8"; 12 | 13 | @Override 14 | public void init(final FilterConfig filterConfig) throws ServletException { 15 | } 16 | 17 | @Override 18 | public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) 19 | throws IOException, ServletException { 20 | request.setCharacterEncoding(DEFAULT_ENCODING); 21 | response.setCharacterEncoding(DEFAULT_ENCODING); 22 | chain.doFilter(request, response); 23 | } 24 | 25 | @Override 26 | public void destroy() { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ControllerHandlerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.asis; 2 | 3 | import com.interface21.webmvc.servlet.ModelAndView; 4 | import com.interface21.webmvc.servlet.mvc.HandlerAdapter; 5 | import com.interface21.webmvc.servlet.view.JspView; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | 9 | public class ControllerHandlerAdapter implements HandlerAdapter { 10 | 11 | @Override 12 | public boolean supports(final Object handler) { 13 | return handler instanceof Controller; 14 | } 15 | 16 | @Override 17 | public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { 18 | final var forwardView = ((Controller) handler).execute(request, response); 19 | return new ModelAndView(new JspView(forwardView)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/ModelAndView.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | public class ModelAndView { 8 | 9 | private final View view; 10 | private final Map model; 11 | 12 | public ModelAndView(final View view) { 13 | this.view = view; 14 | this.model = new HashMap<>(); 15 | } 16 | 17 | public ModelAndView addObject(final String attributeName, final Object attributeValue) { 18 | model.put(attributeName, attributeValue); 19 | return this; 20 | } 21 | 22 | public Object getObject(final String attributeName) { 23 | return model.get(attributeName); 24 | } 25 | 26 | public Map getModel() { 27 | return Collections.unmodifiableMap(model); 28 | } 29 | 30 | public View getView() { 31 | return view; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /study/src/main/java/aop/repository/UserHistoryDao.java: -------------------------------------------------------------------------------- 1 | package aop.repository; 2 | 3 | import aop.domain.UserHistory; 4 | import org.springframework.jdbc.core.JdbcTemplate; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public class UserHistoryDao { 9 | 10 | private final JdbcTemplate jdbcTemplate; 11 | 12 | public UserHistoryDao(final JdbcTemplate jdbcTemplate) { 13 | this.jdbcTemplate = jdbcTemplate; 14 | } 15 | 16 | public void log(final UserHistory userHistory) { 17 | final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; 18 | jdbcTemplate.update(sql, 19 | userHistory.getUserId(), 20 | userHistory.getAccount(), 21 | userHistory.getPassword(), 22 | userHistory.getEmail(), 23 | userHistory.getCreatedAt(), 24 | userHistory.getCreateBy() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.service; 2 | 3 | import com.techcourse.dao.UserDao; 4 | import com.techcourse.dao.UserHistoryDao; 5 | import com.techcourse.domain.User; 6 | import com.techcourse.domain.UserHistory; 7 | 8 | public class UserService { 9 | 10 | private final UserDao userDao; 11 | private final UserHistoryDao userHistoryDao; 12 | 13 | public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { 14 | this.userDao = userDao; 15 | this.userHistoryDao = userHistoryDao; 16 | } 17 | 18 | public User findById(final long id) { 19 | return userDao.findById(id); 20 | } 21 | 22 | public void insert(final User user) { 23 | userDao.insert(user); 24 | } 25 | 26 | public void changePassword(final long id, final String newPassword, final String createBy) { 27 | final var user = findById(id); 28 | user.changePassword(newPassword); 29 | userDao.update(user); 30 | userHistoryDao.log(new UserHistory(user, createBy)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/webapp/js/scripts.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - SB Admin v7.0.2 (https://startbootstrap.com/template/sb-admin) 3 | * Copyright 2013-2021 Start Bootstrap 4 | * Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-sb-admin/blob/master/LICENSE) 5 | */ 6 | // 7 | // Scripts 8 | // 9 | 10 | window.addEventListener('DOMContentLoaded', event => { 11 | 12 | // Toggle the side navigation 13 | const sidebarToggle = document.body.querySelector('#sidebarToggle'); 14 | if (sidebarToggle) { 15 | // Uncomment Below to persist sidebar toggle between refreshes 16 | // if (localStorage.getItem('sb|sidebar-toggle') === 'true') { 17 | // document.body.classList.toggle('sb-sidenav-toggled'); 18 | // } 19 | sidebarToggle.addEventListener('click', event => { 20 | event.preventDefault(); 21 | document.body.classList.toggle('sb-sidenav-toggled'); 22 | localStorage.setItem('sb|sidebar-toggle', document.body.classList.contains('sb-sidenav-toggled')); 23 | }); 24 | } 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.tobe; 2 | 3 | import com.interface21.web.bind.annotation.RequestMethod; 4 | 5 | import java.util.Objects; 6 | 7 | public class HandlerKey { 8 | 9 | private final String url; 10 | private final RequestMethod requestMethod; 11 | 12 | public HandlerKey(final String url, final RequestMethod requestMethod) { 13 | this.url = url; 14 | this.requestMethod = requestMethod; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "HandlerKey [url=" + url + ", requestMethod=" + requestMethod + "]"; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object o) { 24 | if (this == o) return true; 25 | if (!(o instanceof HandlerKey)) return false; 26 | HandlerKey that = (HandlerKey) o; 27 | return Objects.equals(url, that.url) && requestMethod == that.requestMethod; 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | return Objects.hash(url, requestMethod); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage1/UserService.java: -------------------------------------------------------------------------------- 1 | package aop.stage1; 2 | 3 | import aop.Transactional; 4 | import aop.domain.User; 5 | import aop.domain.UserHistory; 6 | import aop.repository.UserDao; 7 | import aop.repository.UserHistoryDao; 8 | 9 | public class UserService { 10 | 11 | private final UserDao userDao; 12 | private final UserHistoryDao userHistoryDao; 13 | 14 | public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { 15 | this.userDao = userDao; 16 | this.userHistoryDao = userHistoryDao; 17 | } 18 | 19 | @Transactional 20 | public User findById(final long id) { 21 | return userDao.findById(id); 22 | } 23 | 24 | @Transactional 25 | public void insert(final User user) { 26 | userDao.insert(user); 27 | } 28 | 29 | @Transactional 30 | public void changePassword(final long id, final String newPassword, final String createBy) { 31 | final var user = findById(id); 32 | user.changePassword(newPassword); 33 | userDao.update(user); 34 | userHistoryDao.log(new UserHistory(user, createBy)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /study/src/main/java/aop/service/AppUserService.java: -------------------------------------------------------------------------------- 1 | package aop.service; 2 | 3 | import aop.Transactional; 4 | import aop.domain.User; 5 | import aop.domain.UserHistory; 6 | import aop.repository.UserDao; 7 | import aop.repository.UserHistoryDao; 8 | 9 | public class AppUserService implements UserService { 10 | 11 | private final UserDao userDao; 12 | private final UserHistoryDao userHistoryDao; 13 | 14 | public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { 15 | this.userDao = userDao; 16 | this.userHistoryDao = userHistoryDao; 17 | } 18 | 19 | @Transactional 20 | public User findById(final long id) { 21 | return userDao.findById(id); 22 | } 23 | 24 | @Transactional 25 | public void insert(final User user) { 26 | userDao.insert(user); 27 | } 28 | 29 | @Transactional 30 | public void changePassword(final long id, final String newPassword, final String createBy) { 31 | final var user = findById(id); 32 | user.changePassword(newPassword); 33 | userDao.update(user); 34 | userHistoryDao.log(new UserHistory(user, createBy)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /study/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.5.5' 4 | id 'io.spring.dependency-management' version '1.1.5' 5 | } 6 | 7 | group 'com.techcourse' 8 | version '1.0-SNAPSHOT' 9 | 10 | java { 11 | sourceCompatibility = JavaVersion.VERSION_21 12 | targetCompatibility = JavaVersion.VERSION_21 13 | } 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | implementation 'org.springframework.boot:spring-boot-starter-web' 21 | implementation 'org.springframework.boot:spring-boot-starter-jdbc' 22 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 23 | 24 | implementation 'com.h2database:h2:2.3.232' 25 | implementation 'org.testcontainers:mysql:1.21.3' 26 | 27 | implementation 'org.reflections:reflections:0.10.2' 28 | implementation 'ch.qos.logback:logback-classic:1.5.18' 29 | implementation 'org.apache.commons:commons-lang3:3.18.0' 30 | implementation 'org.apache.commons:commons-compress:1.26.2' 31 | 32 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 33 | testImplementation 'com.mysql:mysql-connector-j:9.1.0' 34 | } 35 | 36 | test { 37 | maxParallelForks 3 38 | useJUnitPlatform() 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/webapp/assets/chart-bar.js: -------------------------------------------------------------------------------- 1 | // Set new default font family and font color to mimic Bootstrap's default styling 2 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 3 | Chart.defaults.global.defaultFontColor = '#292b2c'; 4 | 5 | // Bar Chart Example 6 | var ctx = document.getElementById("myBarChart"); 7 | var myLineChart = new Chart(ctx, { 8 | type: 'bar', 9 | data: { 10 | labels: ["January", "February", "March", "April", "May", "June"], 11 | datasets: [{ 12 | label: "Revenue", 13 | backgroundColor: "rgba(2,117,216,1)", 14 | borderColor: "rgba(2,117,216,1)", 15 | data: [4215, 5312, 6251, 7841, 9821, 14984], 16 | }], 17 | }, 18 | options: { 19 | scales: { 20 | xAxes: [{ 21 | time: { 22 | unit: 'month' 23 | }, 24 | gridLines: { 25 | display: false 26 | }, 27 | ticks: { 28 | maxTicksLimit: 6 29 | } 30 | }], 31 | yAxes: [{ 32 | ticks: { 33 | min: 0, 34 | max: 15000, 35 | maxTicksLimit: 5 36 | }, 37 | gridLines: { 38 | display: true 39 | } 40 | }], 41 | }, 42 | legend: { 43 | display: false 44 | } 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage2/UserService.java: -------------------------------------------------------------------------------- 1 | package aop.stage2; 2 | 3 | import aop.Transactional; 4 | import aop.domain.User; 5 | import aop.domain.UserHistory; 6 | import aop.repository.UserDao; 7 | import aop.repository.UserHistoryDao; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public class UserService { 12 | 13 | private final UserDao userDao; 14 | private UserHistoryDao userHistoryDao; 15 | 16 | public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { 17 | this.userDao = userDao; 18 | this.userHistoryDao = userHistoryDao; 19 | } 20 | 21 | @Transactional 22 | public User findById(final long id) { 23 | return userDao.findById(id); 24 | } 25 | 26 | @Transactional 27 | public void insert(final User user) { 28 | userDao.insert(user); 29 | } 30 | 31 | @Transactional 32 | public void changePassword(final long id, final String newPassword, final String createBy) { 33 | final var user = findById(id); 34 | user.changePassword(newPassword); 35 | userDao.update(user); 36 | userHistoryDao.log(new UserHistory(user, createBy)); 37 | } 38 | 39 | public void setUserHistoryDao(final UserHistoryDao userHistoryDao) { 40 | this.userHistoryDao = userHistoryDao; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mvc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.interface21' 6 | version '1.0-SNAPSHOT' 7 | 8 | java { 9 | sourceCompatibility = JavaVersion.VERSION_21 10 | targetCompatibility = JavaVersion.VERSION_21 11 | } 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | implementation 'jakarta.servlet:jakarta.servlet-api:6.1.0' 19 | implementation 'jakarta.servlet.jsp:jakarta.servlet.jsp-api:4.0.0' 20 | implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2' 21 | implementation 'jakarta.annotation:jakarta.annotation-api:3.0.0' 22 | annotationProcessor 'jakarta.annotation:jakarta.annotation-api:3.0.0' 23 | 24 | implementation 'org.reflections:reflections:0.10.2' 25 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.19.0' 26 | implementation 'ch.qos.logback:logback-classic:1.5.18' 27 | implementation 'org.apache.commons:commons-lang3:3.18.0' 28 | 29 | testImplementation 'org.assertj:assertj-core:3.26.3' 30 | testImplementation 'org.mockito:mockito-core:5.15.2' 31 | testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4' 32 | testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.4' 33 | testImplementation 'org.springframework:spring-test:6.2.10' 34 | } 35 | 36 | test { 37 | useJUnitPlatform() 38 | } 39 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.tobe; 2 | 3 | import com.interface21.webmvc.servlet.ModelAndView; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | 12 | public class HandlerExecution { 13 | 14 | private static final Logger log = LoggerFactory.getLogger(HandlerExecution.class); 15 | 16 | private final Object declaredObject; 17 | private final Method method; 18 | 19 | public HandlerExecution(final Object declaredObject, final Method method) { 20 | this.declaredObject = declaredObject; 21 | this.method = method; 22 | } 23 | 24 | public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) { 25 | try { 26 | return (ModelAndView) method.invoke(declaredObject, request, response); 27 | } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 28 | log.error("{} method invoke fail. error message : {}", method, e.getMessage()); 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.controller; 2 | 3 | import com.techcourse.repository.InMemoryUserRepository; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import com.interface21.webmvc.servlet.view.JsonView; 7 | import com.interface21.webmvc.servlet.ModelAndView; 8 | import com.interface21.context.stereotype.Controller; 9 | import com.interface21.web.bind.annotation.RequestMapping; 10 | import com.interface21.web.bind.annotation.RequestMethod; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | @Controller 15 | public class UserController { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(UserController.class); 18 | 19 | @RequestMapping(value = "/api/user", method = RequestMethod.GET) 20 | public ModelAndView show(final HttpServletRequest request, final HttpServletResponse response) { 21 | final var account = request.getParameter("account"); 22 | log.debug("user id : {}", account); 23 | 24 | final var modelAndView = new ModelAndView(new JsonView()); 25 | final var user = InMemoryUserRepository.findByAccount(account) 26 | .orElseThrow(); 27 | 28 | modelAndView.addObject("user", user); 29 | return modelAndView; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /study/src/main/java/connectionpool/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package connectionpool; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import javax.sql.DataSource; 9 | 10 | @Configuration 11 | public class DataSourceConfig { 12 | 13 | public static final int MAXIMUM_POOL_SIZE = 5; 14 | private static final String H2_URL = "jdbc:h2:./test;DB_CLOSE_DELAY=-1"; 15 | private static final String USER = "sa"; 16 | private static final String PASSWORD = ""; 17 | 18 | 19 | @Bean 20 | public DataSource hikariDataSource() { 21 | final var hikariConfig = new HikariConfig(); 22 | hikariConfig.setPoolName("gugu"); 23 | hikariConfig.setJdbcUrl(H2_URL); 24 | hikariConfig.setUsername(USER); 25 | hikariConfig.setPassword(PASSWORD); 26 | hikariConfig.setMaximumPoolSize(MAXIMUM_POOL_SIZE); 27 | hikariConfig.setConnectionTestQuery("VALUES 1"); 28 | hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); 29 | hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); 30 | hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 31 | 32 | return new HikariDataSource(hikariConfig); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/controller/RegisterController.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.controller; 2 | 3 | import com.techcourse.domain.User; 4 | import com.techcourse.repository.InMemoryUserRepository; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import com.interface21.webmvc.servlet.view.JspView; 8 | import com.interface21.webmvc.servlet.ModelAndView; 9 | import com.interface21.context.stereotype.Controller; 10 | import com.interface21.web.bind.annotation.RequestMapping; 11 | import com.interface21.web.bind.annotation.RequestMethod; 12 | 13 | @Controller 14 | public class RegisterController { 15 | 16 | @RequestMapping(value = "/register", method = RequestMethod.POST) 17 | public ModelAndView register(final HttpServletRequest request, final HttpServletResponse response) { 18 | final var user = new User(2, 19 | request.getParameter("account"), 20 | request.getParameter("password"), 21 | request.getParameter("email")); 22 | InMemoryUserRepository.save(user); 23 | 24 | return new ModelAndView(new JspView("redirect:/index.jsp")); 25 | } 26 | 27 | @RequestMapping(value = "/register", method = RequestMethod.GET) 28 | public ModelAndView view(final HttpServletRequest request, final HttpServletResponse response) { 29 | return new ModelAndView(new JspView("/register.jsp")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/ManualHandlerMapping.java: -------------------------------------------------------------------------------- 1 | package com.techcourse; 2 | 3 | import com.techcourse.controller.*; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import com.interface21.webmvc.servlet.mvc.HandlerMapping; 6 | import com.interface21.webmvc.servlet.mvc.asis.Controller; 7 | import com.interface21.webmvc.servlet.mvc.asis.ForwardController; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class ManualHandlerMapping implements HandlerMapping { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class); 17 | 18 | private static final Map controllers = new HashMap<>(); 19 | 20 | @Override 21 | public void initialize() { 22 | controllers.put("/", new ForwardController("/index.jsp")); 23 | controllers.put("/logout", new LogoutController()); 24 | 25 | log.info("Initialized Handler Mapping!"); 26 | controllers.keySet() 27 | .forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass())); 28 | } 29 | 30 | @Override 31 | public Controller getHandler(final HttpServletRequest request) { 32 | final var requestURI = request.getRequestURI(); 33 | log.debug("Request Mapping Uri : {}", requestURI); 34 | return controllers.get(requestURI); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /jdbc/src/main/java/com/interface21/jdbc/datasource/DataSourceUtils.java: -------------------------------------------------------------------------------- 1 | package com.interface21.jdbc.datasource; 2 | 3 | import com.interface21.jdbc.CannotGetJdbcConnectionException; 4 | import com.interface21.transaction.support.TransactionSynchronizationManager; 5 | 6 | import javax.sql.DataSource; 7 | import java.sql.Connection; 8 | import java.sql.SQLException; 9 | 10 | // 4단계 미션에서 사용할 것 11 | public abstract class DataSourceUtils { 12 | 13 | private DataSourceUtils() {} 14 | 15 | public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { 16 | Connection connection = TransactionSynchronizationManager.getResource(dataSource); 17 | if (connection != null) { 18 | return connection; 19 | } 20 | 21 | try { 22 | connection = dataSource.getConnection(); 23 | TransactionSynchronizationManager.bindResource(dataSource, connection); 24 | return connection; 25 | } catch (SQLException ex) { 26 | throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex); 27 | } 28 | } 29 | 30 | public static void releaseConnection(Connection connection, DataSource dataSource) { 31 | try { 32 | connection.close(); 33 | } catch (SQLException ex) { 34 | throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/Application.java: -------------------------------------------------------------------------------- 1 | package com.techcourse; 2 | 3 | import org.apache.catalina.connector.Connector; 4 | import org.apache.catalina.startup.Tomcat; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.File; 9 | import java.util.stream.Stream; 10 | 11 | public class Application { 12 | 13 | private static final Logger log = LoggerFactory.getLogger(Application.class); 14 | 15 | private static final int DEFAULT_PORT = 8080; 16 | 17 | public static void main(String[] args) throws Exception { 18 | final int port = defaultPortIfNull(args); 19 | 20 | final var tomcat = new Tomcat(); 21 | tomcat.setConnector(createConnector(port)); 22 | final var docBase = new File("app/src/main/webapp/").getAbsolutePath(); 23 | tomcat.addWebapp("", docBase); 24 | log.info("configuring app with basedir: {}", docBase); 25 | 26 | tomcat.start(); 27 | tomcat.getServer().await(); 28 | } 29 | 30 | private static Connector createConnector(final int port) { 31 | final var connector = new Connector(); 32 | connector.setPort(port); 33 | connector.setProperty("bindOnInit", "false"); 34 | return connector; 35 | } 36 | 37 | private static int defaultPortIfNull(String[] args) { 38 | return Stream.of(args) 39 | .findFirst() 40 | .map(Integer::parseInt) 41 | .orElse(DEFAULT_PORT); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | } 5 | 6 | group 'com.techcourse' 7 | version '1.0-SNAPSHOT' 8 | 9 | java { 10 | sourceCompatibility = JavaVersion.VERSION_21 11 | targetCompatibility = JavaVersion.VERSION_21 12 | } 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | implementation project(':mvc') 20 | implementation project(':jdbc') 21 | 22 | implementation 'org.springframework:spring-tx:6.2.10' 23 | implementation 'org.springframework:spring-jdbc:6.2.10' 24 | 25 | implementation 'org.apache.tomcat.embed:tomcat-embed-core:11.0.10' 26 | implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:11.0.10' 27 | implementation 'ch.qos.logback:logback-classic:1.5.18' 28 | implementation 'org.apache.commons:commons-lang3:3.18.0' 29 | implementation 'com.h2database:h2:2.3.232' 30 | 31 | testImplementation 'org.assertj:assertj-core:3.26.3' 32 | testImplementation 'org.mockito:mockito-core:5.15.2' 33 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.13.4' 34 | testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.4' 35 | } 36 | 37 | test { 38 | useJUnitPlatform() 39 | } 40 | 41 | idea { 42 | module { 43 | inheritOutputDirs = false 44 | outputDir file('src/main/webapp/WEB-INF/classes') 45 | } 46 | } 47 | 48 | sourceSets { 49 | main { 50 | java.destinationDirectory.set(file('src/main/webapp/WEB-INF/classes')) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /study/src/main/java/aop/domain/User.java: -------------------------------------------------------------------------------- 1 | package aop.domain; 2 | 3 | public class User { 4 | 5 | private Long id; 6 | private final String account; 7 | private String password; 8 | private final String email; 9 | 10 | public User(long id, String account, String password, String email) { 11 | this.id = id; 12 | this.account = account; 13 | this.password = password; 14 | this.email = email; 15 | } 16 | 17 | public User(String account, String password, String email) { 18 | this.account = account; 19 | this.password = password; 20 | this.email = email; 21 | } 22 | 23 | public boolean checkPassword(String password) { 24 | return this.password.equals(password); 25 | } 26 | 27 | public void changePassword(String password) { 28 | this.password = password; 29 | } 30 | 31 | public String getAccount() { 32 | return account; 33 | } 34 | 35 | public long getId() { 36 | return id; 37 | } 38 | 39 | public String getEmail() { 40 | return email; 41 | } 42 | 43 | public String getPassword() { 44 | return password; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "User{" + 50 | "id=" + id + 51 | ", account='" + account + '\'' + 52 | ", email='" + email + '\'' + 53 | ", password='" + password + '\'' + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/User.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1; 2 | 3 | public class User { 4 | 5 | private Long id; 6 | private final String account; 7 | private String password; 8 | private final String email; 9 | 10 | public User(long id, String account, String password, String email) { 11 | this.id = id; 12 | this.account = account; 13 | this.password = password; 14 | this.email = email; 15 | } 16 | 17 | public User(String account, String password, String email) { 18 | this.account = account; 19 | this.password = password; 20 | this.email = email; 21 | } 22 | 23 | public boolean checkPassword(String password) { 24 | return this.password.equals(password); 25 | } 26 | 27 | public void changePassword(String password) { 28 | this.password = password; 29 | } 30 | 31 | public String getAccount() { 32 | return account; 33 | } 34 | 35 | public long getId() { 36 | return id; 37 | } 38 | 39 | public String getEmail() { 40 | return email; 41 | } 42 | 43 | public String getPassword() { 44 | return password; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "User{" + 50 | "id=" + id + 51 | ", account='" + account + '\'' + 52 | ", email='" + email + '\'' + 53 | ", password='" + password + '\'' + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.domain; 2 | 3 | public class User { 4 | 5 | private Long id; 6 | private final String account; 7 | private String password; 8 | private final String email; 9 | 10 | public User(long id, String account, String password, String email) { 11 | this.id = id; 12 | this.account = account; 13 | this.password = password; 14 | this.email = email; 15 | } 16 | 17 | public User(String account, String password, String email) { 18 | this.account = account; 19 | this.password = password; 20 | this.email = email; 21 | } 22 | 23 | public boolean checkPassword(String password) { 24 | return this.password.equals(password); 25 | } 26 | 27 | public void changePassword(String password) { 28 | this.password = password; 29 | } 30 | 31 | public String getAccount() { 32 | return account; 33 | } 34 | 35 | public long getId() { 36 | return id; 37 | } 38 | 39 | public String getEmail() { 40 | return email; 41 | } 42 | 43 | public String getPassword() { 44 | return password; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return "User{" + 50 | "id=" + id + 51 | ", account='" + account + '\'' + 52 | ", email='" + email + '\'' + 53 | ", password='" + password + '\'' + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/AppWebApplicationInitializer.java: -------------------------------------------------------------------------------- 1 | package com.techcourse; 2 | 3 | import jakarta.servlet.ServletContext; 4 | import com.interface21.webmvc.servlet.mvc.DispatcherServlet; 5 | import com.interface21.webmvc.servlet.mvc.asis.ControllerHandlerAdapter; 6 | import com.interface21.webmvc.servlet.mvc.tobe.AnnotationHandlerMapping; 7 | import com.interface21.webmvc.servlet.mvc.tobe.HandlerExecutionHandlerAdapter; 8 | import com.interface21.web.WebApplicationInitializer; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class AppWebApplicationInitializer implements WebApplicationInitializer { 13 | 14 | private static final Logger log = LoggerFactory.getLogger(AppWebApplicationInitializer.class); 15 | 16 | @Override 17 | public void onStartup(final ServletContext servletContext) { 18 | final var dispatcherServlet = new DispatcherServlet(); 19 | dispatcherServlet.addHandlerMapping(new ManualHandlerMapping()); 20 | dispatcherServlet.addHandlerMapping(new AnnotationHandlerMapping("com.techcourse.controller")); 21 | 22 | dispatcherServlet.addHandlerAdapter(new ControllerHandlerAdapter()); 23 | dispatcherServlet.addHandlerAdapter(new HandlerExecutionHandlerAdapter()); 24 | 25 | final var dispatcher = servletContext.addServlet("dispatcher", dispatcherServlet); 26 | dispatcher.setLoadOnStartup(1); 27 | dispatcher.addMapping("/"); 28 | 29 | log.info("Start AppWebApplication Initializer"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.view; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import com.interface21.webmvc.servlet.View; 8 | 9 | import java.util.Map; 10 | import java.util.Objects; 11 | 12 | public class JspView implements View { 13 | 14 | private static final Logger log = LoggerFactory.getLogger(JspView.class); 15 | 16 | public static final String REDIRECT_PREFIX = "redirect:"; 17 | 18 | private final String viewName; 19 | 20 | public JspView(final String viewName) { 21 | this.viewName = Objects.requireNonNull(viewName, "viewName is null. 이동할 URL을 입력하세요."); 22 | } 23 | 24 | @Override 25 | public void render(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { 26 | log.debug("ViewName : {}", viewName); 27 | if (viewName.startsWith(REDIRECT_PREFIX)) { 28 | response.sendRedirect(viewName.substring(REDIRECT_PREFIX.length())); 29 | return; 30 | } 31 | 32 | model.keySet().forEach(key -> { 33 | log.debug("attribute name : {}, value : {}", key, model.get(key)); 34 | request.setAttribute(key, model.get(key)); 35 | }); 36 | 37 | final var requestDispatcher = request.getRequestDispatcher(viewName); 38 | requestDispatcher.forward(request, response); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.view; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.servlet.ServletOutputStream; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import com.interface21.web.http.MediaType; 8 | import com.interface21.webmvc.servlet.View; 9 | 10 | import java.io.IOException; 11 | import java.util.Map; 12 | 13 | public class JsonView implements View { 14 | 15 | @Override 16 | public void render(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { 17 | if (model == null || model.isEmpty()) { 18 | return; 19 | } 20 | 21 | response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); 22 | 23 | final Object renderObject = toJsonObject(model); 24 | render(renderObject, response.getOutputStream()); 25 | } 26 | 27 | private void render(final Object renderObject, final ServletOutputStream outputStream) throws IOException { 28 | final ObjectMapper mapper = new ObjectMapper(); 29 | mapper.writeValue(outputStream, renderObject); 30 | } 31 | 32 | private Object toJsonObject(final Map model) { 33 | if (model.size() == 1) { 34 | return model.values() 35 | .stream() 36 | .findFirst() 37 | .orElseThrow(IllegalArgumentException::new); 38 | } 39 | return model; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/ControllerScanner.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.tobe; 2 | 3 | import com.interface21.context.stereotype.Controller; 4 | import org.reflections.Reflections; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | public class ControllerScanner { 14 | 15 | private static final Logger log = LoggerFactory.getLogger(ControllerScanner.class); 16 | 17 | private final Reflections reflections; 18 | 19 | public ControllerScanner(final Object... basePackage) { 20 | reflections = new Reflections(basePackage); 21 | } 22 | 23 | public Map, Object> getControllers() { 24 | final var preInitiatedControllers = reflections.getTypesAnnotatedWith(Controller.class); 25 | return instantiateControllers(preInitiatedControllers); 26 | } 27 | 28 | Map, Object> instantiateControllers(final Set> preInitiatedControllers) { 29 | final var controllers = new HashMap, Object>(); 30 | try { 31 | for (final var clazz : preInitiatedControllers) { 32 | controllers.put(clazz, clazz.getDeclaredConstructor().newInstance()); 33 | } 34 | } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 35 | log.error(e.getMessage()); 36 | } 37 | 38 | return controllers; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /study/src/main/java/aop/domain/UserHistory.java: -------------------------------------------------------------------------------- 1 | package aop.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class UserHistory { 6 | 7 | private Long id; 8 | 9 | private final long userId; 10 | private final String account; 11 | private final String password; 12 | private final String email; 13 | 14 | private final LocalDateTime createdAt; 15 | 16 | private final String createBy; 17 | 18 | public UserHistory(final User user, final String createBy) { 19 | this(null, user.getId(), user.getAccount(), user.getPassword(), user.getEmail(), createBy); 20 | } 21 | 22 | public UserHistory(final Long id, final long userId, final String account, final String password, final String email, final String createBy) { 23 | this.id = id; 24 | this.userId = userId; 25 | this.account = account; 26 | this.password = password; 27 | this.email = email; 28 | this.createdAt = LocalDateTime.now(); 29 | this.createBy = createBy; 30 | } 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public long getUserId() { 37 | return userId; 38 | } 39 | 40 | public String getAccount() { 41 | return account; 42 | } 43 | 44 | public String getPassword() { 45 | return password; 46 | } 47 | 48 | public String getEmail() { 49 | return email; 50 | } 51 | 52 | public LocalDateTime getCreatedAt() { 53 | return createdAt; 54 | } 55 | 56 | public String getCreateBy() { 57 | return createBy; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/core/util/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.interface21.core.util; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.Modifier; 5 | 6 | public abstract class ReflectionUtils { 7 | 8 | /** 9 | * Obtain an accessible constructor for the given class and parameters. 10 | * @param clazz the clazz to check 11 | * @param parameterTypes the parameter types of the desired constructor 12 | * @return the constructor reference 13 | * @throws NoSuchMethodException if no such constructor exists 14 | * @since 5.0 15 | */ 16 | public static Constructor accessibleConstructor(Class clazz, Class>... parameterTypes) 17 | throws NoSuchMethodException { 18 | 19 | Constructor ctor = clazz.getDeclaredConstructor(parameterTypes); 20 | makeAccessible(ctor); 21 | return ctor; 22 | } 23 | 24 | /** 25 | * Make the given constructor accessible, explicitly setting it accessible 26 | * if necessary. The {@code setAccessible(true)} method is only called 27 | * when actually necessary, to avoid unnecessary conflicts. 28 | * @param ctor the constructor to make accessible 29 | * @see Constructor#setAccessible 30 | */ 31 | @SuppressWarnings("deprecation") 32 | public static void makeAccessible(Constructor> ctor) { 33 | if ((!Modifier.isPublic(ctor.getModifiers()) || 34 | !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && !ctor.isAccessible()) { 35 | ctor.setAccessible(true); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/domain/UserHistory.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class UserHistory { 6 | 7 | private Long id; 8 | 9 | private final long userId; 10 | private final String account; 11 | private final String password; 12 | private final String email; 13 | 14 | private final LocalDateTime createdAt; 15 | 16 | private final String createBy; 17 | 18 | public UserHistory(final User user, final String createBy) { 19 | this(null, user.getId(), user.getAccount(), user.getPassword(), user.getEmail(), createBy); 20 | } 21 | 22 | public UserHistory(final Long id, final long userId, final String account, final String password, final String email, final String createBy) { 23 | this.id = id; 24 | this.userId = userId; 25 | this.account = account; 26 | this.password = password; 27 | this.email = email; 28 | this.createdAt = LocalDateTime.now(); 29 | this.createBy = createBy; 30 | } 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public long getUserId() { 37 | return userId; 38 | } 39 | 40 | public String getAccount() { 41 | return account; 42 | } 43 | 44 | public String getPassword() { 45 | return password; 46 | } 47 | 48 | public String getEmail() { 49 | return email; 50 | } 51 | 52 | public LocalDateTime getCreatedAt() { 53 | return createdAt; 54 | } 55 | 56 | public String getCreateBy() { 57 | return createBy; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /study/src/main/java/transaction/DatabasePopulatorUtils.java: -------------------------------------------------------------------------------- 1 | package transaction; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.sql.DataSource; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.sql.Connection; 11 | import java.sql.SQLException; 12 | import java.sql.Statement; 13 | 14 | public class DatabasePopulatorUtils { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(DatabasePopulatorUtils.class); 17 | 18 | public static void execute(final DataSource dataSource) { 19 | Connection connection = null; 20 | Statement statement = null; 21 | try { 22 | final var url = DatabasePopulatorUtils.class.getClassLoader().getResource("schema.sql"); 23 | final var file = new File(url.getFile()); 24 | final var sql = Files.readString(file.toPath()); 25 | connection = dataSource.getConnection(); 26 | statement = connection.createStatement(); 27 | statement.execute(sql); 28 | } catch (NullPointerException | IOException | SQLException e) { 29 | log.error(e.getMessage(), e.getCause()); 30 | } finally { 31 | try { 32 | if (statement != null) { 33 | statement.close(); 34 | } 35 | } catch (SQLException ignored) {} 36 | 37 | try { 38 | if (connection != null) { 39 | connection.close(); 40 | } 41 | } catch (SQLException ignored) {} 42 | } 43 | } 44 | 45 | private DatabasePopulatorUtils() {} 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/support/jdbc/init/DatabasePopulatorUtils.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.support.jdbc.init; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.sql.DataSource; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.sql.Connection; 11 | import java.sql.SQLException; 12 | import java.sql.Statement; 13 | 14 | public class DatabasePopulatorUtils { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(DatabasePopulatorUtils.class); 17 | 18 | public static void execute(final DataSource dataSource) { 19 | Connection connection = null; 20 | Statement statement = null; 21 | try { 22 | final var url = DatabasePopulatorUtils.class.getClassLoader().getResource("schema.sql"); 23 | final var file = new File(url.getFile()); 24 | final var sql = Files.readString(file.toPath()); 25 | connection = dataSource.getConnection(); 26 | statement = connection.createStatement(); 27 | statement.execute(sql); 28 | } catch (NullPointerException | IOException | SQLException e) { 29 | log.error(e.getMessage(), e); 30 | } finally { 31 | try { 32 | if (statement != null) { 33 | statement.close(); 34 | } 35 | } catch (SQLException ignored) {} 36 | 37 | try { 38 | if (connection != null) { 39 | connection.close(); 40 | } 41 | } catch (SQLException ignored) {} 42 | } 43 | } 44 | 45 | private DatabasePopulatorUtils() {} 46 | } 47 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/web/SpringServletContainerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.interface21.web; 2 | 3 | import com.interface21.core.util.ReflectionUtils; 4 | import jakarta.servlet.ServletContainerInitializer; 5 | import jakarta.servlet.ServletContext; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.annotation.HandlesTypes; 8 | 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | @HandlesTypes(WebApplicationInitializer.class) 14 | public class SpringServletContainerInitializer implements ServletContainerInitializer { 15 | 16 | @Override 17 | public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) 18 | throws ServletException { 19 | final List initializers = new LinkedList<>(); 20 | 21 | if (webAppInitializerClasses != null) { 22 | for (Class> waiClass : webAppInitializerClasses) { 23 | try { 24 | initializers.add((WebApplicationInitializer) 25 | ReflectionUtils.accessibleConstructor(waiClass).newInstance()); 26 | } catch (Throwable ex) { 27 | throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); 28 | } 29 | } 30 | } 31 | 32 | if (initializers.isEmpty()) { 33 | servletContext.log("No Spring WebApplicationInitializer types detected on classpath"); 34 | return; 35 | } 36 | 37 | for (WebApplicationInitializer initializer : initializers) { 38 | initializer.onStartup(servletContext); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage2/User.java: -------------------------------------------------------------------------------- 1 | package transaction.stage2; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.GenerationType; 6 | import jakarta.persistence.Id; 7 | 8 | @Entity(name = "users") 9 | public class User { 10 | 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Long id; 14 | private String account; 15 | private String password; 16 | private String email; 17 | 18 | protected User() {} 19 | 20 | public User(String account, String password, String email) { 21 | this.account = account; 22 | this.password = password; 23 | this.email = email; 24 | } 25 | 26 | public boolean checkPassword(String password) { 27 | return this.password.equals(password); 28 | } 29 | 30 | public void changePassword(String password) { 31 | this.password = password; 32 | } 33 | 34 | public static User createTest() { 35 | return new User("gugu", "password", "hkkang@woowahan.com"); 36 | } 37 | 38 | public Long getId() { 39 | return id; 40 | } 41 | 42 | public String getAccount() { 43 | return account; 44 | } 45 | 46 | public String getPassword() { 47 | return password; 48 | } 49 | 50 | public String getEmail() { 51 | return email; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "User{" + 57 | "id=" + id + 58 | ", account='" + account + '\'' + 59 | ", email='" + email + '\'' + 60 | ", password='" + password + '\'' + 61 | '}'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /study/src/main/java/aop/service/TxUserService.java: -------------------------------------------------------------------------------- 1 | package aop.service; 2 | 3 | import aop.DataAccessException; 4 | import aop.domain.User; 5 | import org.springframework.transaction.PlatformTransactionManager; 6 | import org.springframework.transaction.support.DefaultTransactionDefinition; 7 | 8 | public class TxUserService implements UserService { 9 | 10 | private final PlatformTransactionManager transactionManager; 11 | private final UserService userService; 12 | 13 | public TxUserService(final PlatformTransactionManager transactionManager, final UserService userService) { 14 | this.transactionManager = transactionManager; 15 | this.userService = userService; 16 | } 17 | 18 | @Override 19 | public User findById(final long id) { 20 | return userService.findById(id); 21 | } 22 | 23 | @Override 24 | public void insert(final User user) { 25 | userService.insert(user); 26 | } 27 | 28 | @Override 29 | public void changePassword(final long id, final String newPassword, final String createBy) { 30 | /* ===== 트랜잭션 영역 ===== */ 31 | final var transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); 32 | 33 | try { 34 | /* ===== 트랜잭션 영역 ===== */ 35 | 36 | /* ===== 애플리케이션 영역 ===== */ 37 | userService.changePassword(id, newPassword, createBy); 38 | /* ===== 애플리케이션 영역 ===== */ 39 | 40 | /* ===== 트랜잭션 영역 ===== */ 41 | } catch (RuntimeException e) { 42 | transactionManager.rollback(transactionStatus); 43 | throw new DataAccessException(e); 44 | } 45 | transactionManager.commit(transactionStatus); 46 | /* ===== 트랜잭션 영역 ===== */ 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/webapp/assets/chart-area.js: -------------------------------------------------------------------------------- 1 | // Set new default font family and font color to mimic Bootstrap's default styling 2 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 3 | Chart.defaults.global.defaultFontColor = '#292b2c'; 4 | 5 | // Area Chart Example 6 | var ctx = document.getElementById("myAreaChart"); 7 | var myLineChart = new Chart(ctx, { 8 | type: 'line', 9 | data: { 10 | labels: ["Mar 1", "Mar 2", "Mar 3", "Mar 4", "Mar 5", "Mar 6", "Mar 7", "Mar 8", "Mar 9", "Mar 10", "Mar 11", "Mar 12", "Mar 13"], 11 | datasets: [{ 12 | data: [10000, 30162, 26263, 18394, 18287, 28682, 31274, 33259, 25849, 24159, 32651, 31984, 38451], 13 | label: "Sessions", 14 | lineTension: 0.3, 15 | backgroundColor: "rgba(2,117,216,0.2)", 16 | borderColor: "rgba(2,117,216,1)", 17 | pointRadius: 5, 18 | pointBackgroundColor: "rgba(2,117,216,1)", 19 | pointBorderColor: "rgba(255,255,255,0.8)", 20 | pointHoverRadius: 5, 21 | pointHoverBackgroundColor: "rgba(2,117,216,1)", 22 | pointHitRadius: 50, 23 | pointBorderWidth: 2, 24 | }], 25 | }, 26 | options: { 27 | scales: { 28 | xAxes: [{ 29 | time: { 30 | unit: 'date' 31 | }, 32 | gridLines: { 33 | display: false 34 | }, 35 | ticks: { 36 | maxTicksLimit: 7 37 | } 38 | }], 39 | yAxes: [{ 40 | ticks: { 41 | min: 0, 42 | max: 40000, 43 | maxTicksLimit: 5 44 | }, 45 | gridLines: { 46 | color: "rgba(0, 0, 0, .125)", 47 | } 48 | }], 49 | }, 50 | legend: { 51 | display: false 52 | } 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage2/Stage2Test.java: -------------------------------------------------------------------------------- 1 | package aop.stage2; 2 | 3 | import aop.DataAccessException; 4 | import aop.StubUserHistoryDao; 5 | import aop.domain.User; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 17 | class Stage2Test { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(Stage2Test.class); 20 | 21 | @Autowired 22 | private UserService userService; 23 | 24 | @Autowired 25 | private StubUserHistoryDao stubUserHistoryDao; 26 | 27 | @BeforeEach 28 | void setUp() { 29 | final var user = new User("gugu", "password", "hkkang@woowahan.com"); 30 | userService.insert(user); 31 | } 32 | 33 | @Test 34 | void testChangePassword() { 35 | final var newPassword = "qqqqq"; 36 | final var createBy = "gugu"; 37 | userService.changePassword(1L, newPassword, createBy); 38 | 39 | final var actual = userService.findById(1L); 40 | 41 | assertThat(actual.getPassword()).isEqualTo(newPassword); 42 | } 43 | 44 | @Test 45 | void testTransactionRollback() { 46 | userService.setUserHistoryDao(stubUserHistoryDao); 47 | 48 | final var newPassword = "newPassword"; 49 | final var createBy = "gugu"; 50 | assertThrows(DataAccessException.class, 51 | () -> userService.changePassword(1L, newPassword, createBy)); 52 | 53 | final var actual = userService.findById(1L); 54 | 55 | assertThat(actual.getPassword()).isNotEqualTo(newPassword); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /study/src/main/java/aop/repository/UserDao.java: -------------------------------------------------------------------------------- 1 | package aop.repository; 2 | 3 | import aop.domain.User; 4 | import org.springframework.jdbc.core.JdbcTemplate; 5 | import org.springframework.jdbc.core.RowMapper; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | @Repository 11 | public class UserDao { 12 | 13 | private final JdbcTemplate jdbcTemplate; 14 | 15 | public UserDao(final JdbcTemplate jdbcTemplate) { 16 | this.jdbcTemplate = jdbcTemplate; 17 | } 18 | 19 | public void insert(final User user) { 20 | final var sql = "insert into users (account, password, email) values (?, ?, ?)"; 21 | jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail()); 22 | } 23 | 24 | public void update(final User user) { 25 | final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; 26 | jdbcTemplate.update(sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); 27 | } 28 | 29 | public List findAll() { 30 | final var sql = "select id, account, password, email from users"; 31 | return jdbcTemplate.query(sql, createRowMapper()); 32 | } 33 | 34 | public User findById(final Long id) { 35 | final var sql = "select id, account, password, email from users where id = ?"; 36 | return jdbcTemplate.queryForObject(sql, createRowMapper(), id); 37 | } 38 | 39 | public User findByAccount(final String account) { 40 | final var sql = "select id, account, password, email from users where account = ?"; 41 | return jdbcTemplate.queryForObject(sql, createRowMapper(), account); 42 | } 43 | 44 | private static RowMapper createRowMapper() { 45 | return (final var rs, final var i) -> new User( 46 | rs.getLong("id"), 47 | rs.getString("account"), 48 | rs.getString("password"), 49 | rs.getString("email")); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/webapp/500.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | <%@ include file="include/header.jspf" %> 6 | 404 Error - SB Admin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 500 17 | Internal Server Error 18 | 19 | 20 | Return to Dashboard 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | <%@ include file="include/footer.jspf" %> 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/test/java/com/techcourse/dao/UserDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.dao; 2 | 3 | import com.techcourse.config.DataSourceConfig; 4 | import com.techcourse.domain.User; 5 | import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class UserDaoTest { 12 | 13 | private UserDao userDao; 14 | 15 | @BeforeEach 16 | void setup() { 17 | DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); 18 | 19 | userDao = new UserDao(DataSourceConfig.getInstance()); 20 | final var user = new User("gugu", "password", "hkkang@woowahan.com"); 21 | userDao.insert(user); 22 | } 23 | 24 | @Test 25 | void findAll() { 26 | final var users = userDao.findAll(); 27 | 28 | assertThat(users).isNotEmpty(); 29 | } 30 | 31 | @Test 32 | void findById() { 33 | final var user = userDao.findById(1L); 34 | 35 | assertThat(user.getAccount()).isEqualTo("gugu"); 36 | } 37 | 38 | @Test 39 | void findByAccount() { 40 | final var account = "gugu"; 41 | final var user = userDao.findByAccount(account); 42 | 43 | assertThat(user.getAccount()).isEqualTo(account); 44 | } 45 | 46 | @Test 47 | void insert() { 48 | final var account = "insert-gugu"; 49 | final var user = new User(account, "password", "hkkang@woowahan.com"); 50 | userDao.insert(user); 51 | 52 | final var actual = userDao.findById(2L); 53 | 54 | assertThat(actual.getAccount()).isEqualTo(account); 55 | } 56 | 57 | @Test 58 | void update() { 59 | final var newPassword = "password99"; 60 | final var user = userDao.findById(1L); 61 | user.changePassword(newPassword); 62 | 63 | userDao.update(user); 64 | 65 | final var actual = userDao.findById(1L); 66 | 67 | assertThat(actual.getPassword()).isEqualTo(newPassword); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/webapp/404.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | <%@ include file="include/header.jspf" %> 6 | 404 Error - SB Admin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | This requested URL was not found on this server. 18 | 19 | 20 | Return to Dashboard 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | <%@ include file="include/footer.jspf" %> 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/webapp/401.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | <%@ include file="include/header.jspf" %> 6 | 404 Error - SB Admin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 401 17 | Unauthorized 18 | Access to this resource is denied. 19 | 20 | 21 | Return to Dashboard 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 43 | 44 | <%@ include file="include/footer.jspf" %> 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/support/web/filter/ResourceFilter.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.support.web.filter; 2 | 3 | import jakarta.servlet.*; 4 | import jakarta.servlet.annotation.WebFilter; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | @WebFilter("/*") 15 | public class ResourceFilter implements Filter { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(ResourceFilter.class); 18 | 19 | private static final List resourcePrefixs = new ArrayList<>(); 20 | 21 | static { 22 | resourcePrefixs.addAll(Arrays.asList( 23 | "/css", 24 | "/js", 25 | "/assets", 26 | "/fonts", 27 | "/images", 28 | "/favicon.ico" 29 | )); 30 | } 31 | 32 | private RequestDispatcher requestDispatcher; 33 | 34 | @Override 35 | public void init(final FilterConfig filterConfig) throws ServletException { 36 | this.requestDispatcher = filterConfig.getServletContext().getNamedDispatcher("default"); 37 | } 38 | 39 | @Override 40 | public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) 41 | throws IOException, ServletException { 42 | final var req = (HttpServletRequest) request; 43 | final var path = req.getRequestURI().substring(req.getContextPath().length()); 44 | if (isResourceUrl(path)) { 45 | log.debug("path : {}", path); 46 | requestDispatcher.forward(request, response); 47 | } else { 48 | chain.doFilter(request, response); 49 | } 50 | } 51 | 52 | private boolean isResourceUrl(final String url) { 53 | for (String prefix : resourcePrefixs) { 54 | if (url.startsWith(prefix)) { 55 | return true; 56 | } 57 | } 58 | return false; 59 | } 60 | 61 | @Override 62 | public void destroy() { 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/dao/UserHistoryDao.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.dao; 2 | 3 | import com.techcourse.domain.UserHistory; 4 | import com.interface21.jdbc.core.JdbcTemplate; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.sql.DataSource; 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.SQLException; 12 | 13 | public class UserHistoryDao { 14 | 15 | private static final Logger log = LoggerFactory.getLogger(UserHistoryDao.class); 16 | 17 | private final DataSource dataSource; 18 | 19 | public UserHistoryDao(final DataSource dataSource) { 20 | this.dataSource = dataSource; 21 | } 22 | 23 | public UserHistoryDao(final JdbcTemplate jdbcTemplate) { 24 | this.dataSource = null; 25 | } 26 | 27 | public void log(final UserHistory userHistory) { 28 | final var sql = "insert into user_history (user_id, account, password, email, created_at, created_by) values (?, ?, ?, ?, ?, ?)"; 29 | 30 | Connection conn = null; 31 | PreparedStatement pstmt = null; 32 | try { 33 | conn = dataSource.getConnection(); 34 | pstmt = conn.prepareStatement(sql); 35 | 36 | log.debug("query : {}", sql); 37 | 38 | pstmt.setLong(1, userHistory.getUserId()); 39 | pstmt.setString(2, userHistory.getAccount()); 40 | pstmt.setString(3, userHistory.getPassword()); 41 | pstmt.setString(4, userHistory.getEmail()); 42 | pstmt.setObject(5, userHistory.getCreatedAt()); 43 | pstmt.setString(6, userHistory.getCreateBy()); 44 | pstmt.executeUpdate(); 45 | } catch (SQLException e) { 46 | log.error(e.getMessage(), e); 47 | throw new RuntimeException(e); 48 | } finally { 49 | try { 50 | if (pstmt != null) { 51 | pstmt.close(); 52 | } 53 | } catch (SQLException ignored) {} 54 | 55 | try { 56 | if (conn != null) { 57 | conn.close(); 58 | } 59 | } catch (SQLException ignored) {} 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage1/Stage1Test.java: -------------------------------------------------------------------------------- 1 | package aop.stage1; 2 | 3 | import aop.DataAccessException; 4 | import aop.StubUserHistoryDao; 5 | import aop.domain.User; 6 | import aop.repository.UserDao; 7 | import aop.repository.UserHistoryDao; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.transaction.PlatformTransactionManager; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertThrows; 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 20 | class Stage1Test { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(Stage1Test.class); 23 | 24 | @Autowired 25 | private UserDao userDao; 26 | 27 | @Autowired 28 | private UserHistoryDao userHistoryDao; 29 | 30 | @Autowired 31 | private StubUserHistoryDao stubUserHistoryDao; 32 | 33 | @Autowired 34 | private PlatformTransactionManager platformTransactionManager; 35 | 36 | @BeforeEach 37 | void setUp() { 38 | final var user = new User("gugu", "password", "hkkang@woowahan.com"); 39 | userDao.insert(user); 40 | } 41 | 42 | @Test 43 | void testChangePassword() { 44 | final UserService userService = null; 45 | 46 | final var newPassword = "qqqqq"; 47 | final var createBy = "gugu"; 48 | userService.changePassword(1L, newPassword, createBy); 49 | 50 | final var actual = userService.findById(1L); 51 | 52 | assertThat(actual.getPassword()).isEqualTo(newPassword); 53 | } 54 | 55 | @Test 56 | void testTransactionRollback() { 57 | final UserService userService = null; 58 | 59 | final var newPassword = "newPassword"; 60 | final var createBy = "gugu"; 61 | assertThrows(DataAccessException.class, 62 | () -> userService.changePassword(1L, newPassword, createBy)); 63 | 64 | final var actual = userService.findById(1L); 65 | 66 | assertThat(actual.getPassword()).isNotEqualTo(newPassword); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /study/src/test/java/connectionpool/stage0/Stage0Test.java: -------------------------------------------------------------------------------- 1 | package connectionpool.stage0; 2 | 3 | import org.h2.jdbcx.JdbcDataSource; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.sql.Connection; 7 | import java.sql.DriverManager; 8 | import java.sql.SQLException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class Stage0Test { 13 | 14 | private static final String H2_URL = "jdbc:h2:./test"; 15 | private static final String USER = "sa"; 16 | private static final String PASSWORD = ""; 17 | 18 | /** 19 | * DriverManager 20 | * JDBC 드라이버를 관리하는 가장 기본적인 방법. 21 | * 커넥션 풀, 분산 트랜잭션을 지원하지 않아서 잘 사용하지 않는다. 22 | * 23 | * JDBC 4.0 이전에는 Class.forName 메서드를 사용하여 JDBC 드라이버를 직접 등록해야 했다. 24 | * JDBC 4.0 부터 DriverManager가 적절한 JDBC 드라이버를 찾는다. 25 | * 26 | * Autoloading of JDBC drivers 27 | * https://docs.oracle.com/javadb/10.8.3.0/ref/rrefjdbc4_0summary.html 28 | */ 29 | @Test 30 | void driverManager() throws SQLException { 31 | // Class.forName("org.h2.Driver"); // JDBC 4.0 부터 생략 가능 32 | // DriverManager 클래스를 활용하여 static 변수의 정보를 활용하여 h2 db에 연결한다. 33 | try (final Connection connection = DriverManager.getConnection(H2_URL, USER, PASSWORD)) { 34 | assertThat(connection.isValid(1)).isTrue(); 35 | } 36 | } 37 | 38 | /** 39 | * DataSource 40 | * 데이터베이스, 파일 같은 물리적 데이터 소스에 연결할 때 사용하는 인터페이스. 41 | * 구현체는 각 vendor에서 제공한다. 42 | * 테스트 코드의 JdbcDataSource 클래스는 h2에서 제공하는 클래스다. 43 | * 44 | * DriverManager가 아닌 DataSource를 사용하는 이유 45 | * - 애플리케이션 코드를 직접 수정하지 않고 properties로 디비 연결을 변경할 수 있다. 46 | * - 커넥션 풀링(Connection pooling) 또는 분산 트랜잭션은 DataSource를 통해서 사용 가능하다. 47 | * 48 | * Using a DataSource Object to Make a Connection 49 | * https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/javax/sql/package-summary.html 50 | */ 51 | @Test 52 | void dataSource() throws SQLException { 53 | final JdbcDataSource dataSource = new JdbcDataSource(); 54 | dataSource.setURL(H2_URL); 55 | dataSource.setUser(USER); 56 | dataSource.setPassword(PASSWORD); 57 | 58 | try (final var connection = dataSource.getConnection()) { 59 | assertThat(connection.isValid(1)).isTrue(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/test/java/com/techcourse/service/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.service; 2 | 3 | import com.techcourse.config.DataSourceConfig; 4 | import com.techcourse.dao.UserDao; 5 | import com.techcourse.dao.UserHistoryDao; 6 | import com.techcourse.domain.User; 7 | import com.techcourse.support.jdbc.init.DatabasePopulatorUtils; 8 | import com.interface21.dao.DataAccessException; 9 | import com.interface21.jdbc.core.JdbcTemplate; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Disabled; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | 17 | @Disabled 18 | class UserServiceTest { 19 | 20 | private JdbcTemplate jdbcTemplate; 21 | private UserDao userDao; 22 | 23 | @BeforeEach 24 | void setUp() { 25 | this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); 26 | this.userDao = new UserDao(jdbcTemplate); 27 | 28 | DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); 29 | final var user = new User("gugu", "password", "hkkang@woowahan.com"); 30 | userDao.insert(user); 31 | } 32 | 33 | @Test 34 | void testChangePassword() { 35 | final var userHistoryDao = new UserHistoryDao(jdbcTemplate); 36 | final var userService = new UserService(userDao, userHistoryDao); 37 | 38 | final var newPassword = "qqqqq"; 39 | final var createBy = "gugu"; 40 | userService.changePassword(1L, newPassword, createBy); 41 | 42 | final var actual = userService.findById(1L); 43 | 44 | assertThat(actual.getPassword()).isEqualTo(newPassword); 45 | } 46 | 47 | @Test 48 | void testTransactionRollback() { 49 | // 트랜잭션 롤백 테스트를 위해 mock으로 교체 50 | final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); 51 | final var userService = new UserService(userDao, userHistoryDao); 52 | 53 | final var newPassword = "newPassword"; 54 | final var createBy = "gugu"; 55 | // 트랜잭션이 정상 동작하는지 확인하기 위해 의도적으로 MockUserHistoryDao에서 예외를 발생시킨다. 56 | assertThrows(DataAccessException.class, 57 | () -> userService.changePassword(1L, newPassword, createBy)); 58 | 59 | final var actual = userService.findById(1L); 60 | 61 | assertThat(actual.getPassword()).isNotEqualTo(newPassword); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/controller/LoginController.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.controller; 2 | 3 | import com.techcourse.domain.User; 4 | import com.techcourse.repository.InMemoryUserRepository; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import com.interface21.webmvc.servlet.view.JspView; 8 | import com.interface21.webmvc.servlet.ModelAndView; 9 | import com.interface21.context.stereotype.Controller; 10 | import com.interface21.web.bind.annotation.RequestMapping; 11 | import com.interface21.web.bind.annotation.RequestMethod; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | @Controller 16 | public class LoginController { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(LoginController.class); 19 | 20 | @RequestMapping(value = "/login", method = RequestMethod.GET) 21 | public ModelAndView view(final HttpServletRequest request, final HttpServletResponse response) { 22 | return UserSession.getUserFrom(request.getSession()) 23 | .map(user -> { 24 | log.info("logged in {}", user.getAccount()); 25 | return redirect("/index.jsp"); 26 | }) 27 | .orElse(new ModelAndView(new JspView("/login.jsp"))); 28 | } 29 | 30 | @RequestMapping(value = "/login", method = RequestMethod.POST) 31 | public ModelAndView login(final HttpServletRequest request, final HttpServletResponse response) { 32 | if (UserSession.isLoggedIn(request.getSession())) { 33 | return redirect("/index.jsp"); 34 | } 35 | 36 | return InMemoryUserRepository.findByAccount(request.getParameter("account")) 37 | .map(user -> { 38 | log.info("User : {}", user); 39 | return login(request, user); 40 | }) 41 | .orElse(redirect("/401.jsp")); 42 | } 43 | 44 | private ModelAndView login(final HttpServletRequest request, final User user) { 45 | if (user.checkPassword(request.getParameter("password"))) { 46 | final var session = request.getSession(); 47 | session.setAttribute(UserSession.SESSION_KEY, user); 48 | return redirect("/index.jsp"); 49 | } else { 50 | return redirect("/401.jsp"); 51 | } 52 | } 53 | 54 | private ModelAndView redirect(final String path) { 55 | return new ModelAndView(new JspView(JspView.REDIRECT_PREFIX + path)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/DispatcherServlet.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc; 2 | 3 | import jakarta.servlet.ServletException; 4 | import jakarta.servlet.http.HttpServlet; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import com.interface21.webmvc.servlet.ModelAndView; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class DispatcherServlet extends HttpServlet { 12 | 13 | private static final long serialVersionUID = 1L; 14 | private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); 15 | 16 | private final HandlerMappingRegistry handlerMappingRegistry; 17 | private final HandlerAdapterRegistry handlerAdapterRegistry; 18 | private HandlerExecutor handlerExecutor; 19 | 20 | public DispatcherServlet() { 21 | handlerMappingRegistry = new HandlerMappingRegistry(); 22 | handlerAdapterRegistry = new HandlerAdapterRegistry(); 23 | } 24 | 25 | @Override 26 | public void init() { 27 | handlerExecutor = new HandlerExecutor(handlerAdapterRegistry); 28 | } 29 | 30 | public void addHandlerMapping(final HandlerMapping handlerMapping) { 31 | handlerMappingRegistry.addHandlerMapping(handlerMapping); 32 | } 33 | 34 | public void addHandlerAdapter(final HandlerAdapter handlerAdapter) { 35 | handlerAdapterRegistry.addHandlerAdapter(handlerAdapter); 36 | } 37 | 38 | @Override 39 | protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { 40 | log.debug("Method : {}, Request URI : {}", request.getMethod(), request.getRequestURI()); 41 | 42 | try { 43 | final var handler = handlerMappingRegistry.getHandler(request); 44 | if (!handler.isPresent()) { 45 | response.setStatus(404); 46 | return; 47 | } 48 | 49 | final var modelAndView = handlerExecutor.handle(request, response, handler.get()); 50 | render(modelAndView, request, response); 51 | } catch (Throwable e) { 52 | log.error("Exception : {}", e.getMessage(), e); 53 | throw new ServletException(e.getMessage()); 54 | } 55 | } 56 | 57 | private void render(final ModelAndView modelAndView, final HttpServletRequest request, final HttpServletResponse response) throws Exception { 58 | final var view = modelAndView.getView(); 59 | view.render(modelAndView.getModel(), request, response); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /study/src/test/java/aop/stage0/Stage0Test.java: -------------------------------------------------------------------------------- 1 | package aop.stage0; 2 | 3 | import aop.DataAccessException; 4 | import aop.StubUserHistoryDao; 5 | import aop.domain.User; 6 | import aop.repository.UserDao; 7 | import aop.repository.UserHistoryDao; 8 | import aop.service.AppUserService; 9 | import aop.service.UserService; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | import org.springframework.transaction.PlatformTransactionManager; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 21 | 22 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 23 | class Stage0Test { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(Stage0Test.class); 26 | 27 | @Autowired 28 | private UserDao userDao; 29 | 30 | @Autowired 31 | private UserHistoryDao userHistoryDao; 32 | 33 | @Autowired 34 | private StubUserHistoryDao stubUserHistoryDao; 35 | 36 | @Autowired 37 | private PlatformTransactionManager platformTransactionManager; 38 | 39 | @BeforeEach 40 | void setUp() { 41 | final var user = new User("gugu", "password", "hkkang@woowahan.com"); 42 | userDao.insert(user); 43 | } 44 | 45 | @Test 46 | void testChangePassword() { 47 | final var appUserService = new AppUserService(userDao, userHistoryDao); 48 | final UserService userService = null; 49 | 50 | final var newPassword = "qqqqq"; 51 | final var createBy = "gugu"; 52 | userService.changePassword(1L, newPassword, createBy); 53 | 54 | final var actual = userService.findById(1L); 55 | 56 | assertThat(actual.getPassword()).isEqualTo(newPassword); 57 | } 58 | 59 | @Test 60 | void testTransactionRollback() { 61 | final var appUserService = new AppUserService(userDao, stubUserHistoryDao); 62 | final UserService userService = null; 63 | 64 | final var newPassword = "newPassword"; 65 | final var createBy = "gugu"; 66 | assertThrows(DataAccessException.class, 67 | () -> userService.changePassword(1L, newPassword, createBy)); 68 | 69 | final var actual = userService.findById(1L); 70 | 71 | assertThat(actual.getPassword()).isNotEqualTo(newPassword); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/UserDao.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1; 2 | 3 | import transaction.stage1.jdbc.JdbcTemplate; 4 | import transaction.stage1.jdbc.RowMapper; 5 | 6 | import javax.sql.DataSource; 7 | import java.sql.Connection; 8 | import java.util.List; 9 | 10 | public class UserDao { 11 | 12 | // spring jdbc가 아닌 직접 구현한 JdbcTemplate을 사용한다. 13 | private final JdbcTemplate jdbcTemplate; 14 | 15 | public UserDao(final DataSource dataSource) { 16 | this.jdbcTemplate = new JdbcTemplate(dataSource); 17 | } 18 | 19 | public void insert(final Connection connection, final User user) { 20 | final var sql = "insert into users (account, password, email) values (?, ?, ?)"; 21 | jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail()); 22 | } 23 | 24 | public void update(final Connection connection, final User user) { 25 | final var sql = "update users set account = ?, password = ?, email = ? where id = ?"; 26 | jdbcTemplate.update(connection, sql, user.getAccount(), user.getPassword(), user.getEmail(), user.getId()); 27 | } 28 | 29 | public void updatePasswordGreaterThan(final Connection connection, final String password, final long id) { 30 | final var sql = "update users set password = ? where id >= ?"; 31 | jdbcTemplate.update(connection, sql, password, id); 32 | } 33 | 34 | public User findById(final Connection connection, final Long id) { 35 | final var sql = "select id, account, password, email from users where id = ?"; 36 | return jdbcTemplate.queryForObject(connection, sql, createRowMapper(), id); 37 | } 38 | 39 | public User findByAccount(final Connection connection, final String account) { 40 | final var sql = "select id, account, password, email from users where account = ?"; 41 | return jdbcTemplate.queryForObject(connection, sql, createRowMapper(), account); 42 | } 43 | 44 | public List findGreaterThan(final Connection connection, final long id) { 45 | final var sql = "select id, account, password, email from users where id >= ?"; 46 | return jdbcTemplate.query(connection, sql, createRowMapper(), id); 47 | } 48 | 49 | public List findByAccountGreaterThan(final Connection connection, final String account) { 50 | final var sql = "select id, account, password, email from users where account >= ?"; 51 | return jdbcTemplate.query(connection, sql, createRowMapper(), account); 52 | } 53 | 54 | public List findAll(final Connection connection) { 55 | final var sql = "select id, account, password, email from users"; 56 | return jdbcTemplate.query(connection, sql, createRowMapper()); 57 | } 58 | 59 | private static RowMapper createRowMapper() { 60 | return (final var rs) -> new User( 61 | rs.getLong("id"), 62 | rs.getString("account"), 63 | rs.getString("password"), 64 | rs.getString("email")); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /study/src/test/java/connectionpool/stage2/Stage2Test.java: -------------------------------------------------------------------------------- 1 | package connectionpool.stage2; 2 | 3 | import com.zaxxer.hikari.HikariDataSource; 4 | import com.zaxxer.hikari.pool.HikariPool; 5 | import connectionpool.DataSourceConfig; 6 | import org.junit.jupiter.api.Test; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | 12 | import javax.sql.DataSource; 13 | import java.lang.reflect.Field; 14 | import java.sql.Connection; 15 | 16 | import static com.zaxxer.hikari.util.UtilityElf.quietlySleep; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 20 | class Stage2Test { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(Stage2Test.class); 23 | 24 | /** 25 | * spring boot에서 설정 파일인 application.yml를 사용하여 DataSource를 설정할 수 있다. 26 | * 하지만 DataSource를 여러 개 사용하거나 세부 설정을 하려면 빈을 직접 생성하는 방법을 사용한다. 27 | * DataSourceConfig 클래스를 찾아서 어떻게 빈으로 직접 생성하는지 확인해보자. 28 | * 그리고 아래 DataSource가 직접 생성한 빈으로 주입 받았는지 getPoolName() 메서드로 확인해보자. 29 | */ 30 | @Autowired 31 | private DataSource dataSource; 32 | 33 | @Test 34 | void test() throws InterruptedException { 35 | final var hikariDataSource = (HikariDataSource) dataSource; 36 | final var hikariPool = getPool((HikariDataSource) dataSource); 37 | 38 | // 설정한 커넥션 풀 최대값보다 더 많은 스레드를 생성해서 동시에 디비에 접근을 시도하면 어떻게 될까? 39 | final var threads = new Thread[20]; 40 | for (int i = 0; i < threads.length; i++) { 41 | threads[i] = new Thread(getConnection()); 42 | } 43 | 44 | for (final var thread : threads) { 45 | thread.start(); 46 | } 47 | 48 | for (final var thread : threads) { 49 | thread.join(); 50 | } 51 | 52 | // 동시에 많은 요청이 몰려도 최대 풀 사이즈를 유지한다. 53 | assertThat(hikariPool.getTotalConnections()).isEqualTo(DataSourceConfig.MAXIMUM_POOL_SIZE); 54 | 55 | // DataSourceConfig 클래스에서 직접 생성한 커넥션 풀. 56 | assertThat(hikariDataSource.getPoolName()).isEqualTo("gugu"); 57 | } 58 | 59 | // 데이터베이스에 연결만 하는 메서드. 커넥션 풀에 몇 개의 연결이 생기는지 확인하는 용도. 60 | private Runnable getConnection() { 61 | return () -> { 62 | try { 63 | log.info("Before acquire "); 64 | try (Connection ignored = dataSource.getConnection()) { 65 | log.info("After acquire "); 66 | quietlySleep(500); // Thread.sleep(500)과 동일한 기능 67 | } 68 | } catch (Exception e) { 69 | } 70 | }; 71 | } 72 | 73 | // 학습 테스트를 위해 HikariPool을 추출 74 | public static HikariPool getPool(final HikariDataSource hikariDataSource) 75 | { 76 | try { 77 | Field field = hikariDataSource.getClass().getDeclaredField("pool"); 78 | field.setAccessible(true); 79 | return (HikariPool) field.get(hikariDataSource); 80 | } catch (Exception e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage2/SecondUserService.java: -------------------------------------------------------------------------------- 1 | package transaction.stage2; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Propagation; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import org.springframework.transaction.support.TransactionSynchronizationManager; 9 | 10 | @Service 11 | public class SecondUserService { 12 | 13 | private static final Logger log = LoggerFactory.getLogger(SecondUserService.class); 14 | 15 | private final UserRepository userRepository; 16 | 17 | public SecondUserService(final UserRepository userRepository) { 18 | this.userRepository = userRepository; 19 | } 20 | 21 | @Transactional(propagation = Propagation.REQUIRED) 22 | public String saveSecondTransactionWithRequired() { 23 | userRepository.save(User.createTest()); 24 | logActualTransactionActive(); 25 | return TransactionSynchronizationManager.getCurrentTransactionName(); 26 | } 27 | 28 | @Transactional(propagation = Propagation.REQUIRES_NEW) 29 | public String saveSecondTransactionWithRequiresNew() { 30 | userRepository.save(User.createTest()); 31 | logActualTransactionActive(); 32 | return TransactionSynchronizationManager.getCurrentTransactionName(); 33 | } 34 | 35 | @Transactional(propagation = Propagation.SUPPORTS) 36 | public String saveSecondTransactionWithSupports() { 37 | userRepository.save(User.createTest()); 38 | logActualTransactionActive(); 39 | return TransactionSynchronizationManager.getCurrentTransactionName(); 40 | } 41 | 42 | @Transactional(propagation = Propagation.MANDATORY) 43 | public String saveSecondTransactionWithMandatory() { 44 | userRepository.save(User.createTest()); 45 | logActualTransactionActive(); 46 | return TransactionSynchronizationManager.getCurrentTransactionName(); 47 | } 48 | 49 | @Transactional(propagation = Propagation.NOT_SUPPORTED) 50 | public String saveSecondTransactionWithNotSupported() { 51 | userRepository.save(User.createTest()); 52 | logActualTransactionActive(); 53 | return TransactionSynchronizationManager.getCurrentTransactionName(); 54 | } 55 | 56 | @Transactional(propagation = Propagation.NESTED) 57 | public String saveSecondTransactionWithNested() { 58 | userRepository.save(User.createTest()); 59 | logActualTransactionActive(); 60 | return TransactionSynchronizationManager.getCurrentTransactionName(); 61 | } 62 | 63 | @Transactional(propagation = Propagation.NEVER) 64 | public String saveSecondTransactionWithNever() { 65 | userRepository.save(User.createTest()); 66 | logActualTransactionActive(); 67 | return TransactionSynchronizationManager.getCurrentTransactionName(); 68 | } 69 | 70 | private void logActualTransactionActive() { 71 | final var currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 72 | final var actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); 73 | final var emoji = actualTransactionActive ? "✅" : "❌"; 74 | log.info("\n{} is Actual Transaction Active : {} {}", currentTransactionName, emoji, actualTransactionActive); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/webapp/login.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | <%@ include file="include/header.jspf" %> 6 | 로그인 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 로그인 17 | 18 | 19 | 20 | 21 | 아이디 22 | 23 | 24 | 25 | 비밀번호 26 | 27 | 28 | 로그인 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 57 | 58 | <%@ include file="include/footer.jspf" %> 59 | 60 | 61 | -------------------------------------------------------------------------------- /mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java: -------------------------------------------------------------------------------- 1 | package com.interface21.webmvc.servlet.mvc.tobe; 2 | 3 | import com.interface21.webmvc.servlet.mvc.HandlerMapping; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import com.interface21.web.bind.annotation.RequestMapping; 6 | import com.interface21.web.bind.annotation.RequestMethod; 7 | import org.reflections.ReflectionUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.*; 13 | import java.util.stream.Collectors; 14 | 15 | public class AnnotationHandlerMapping implements HandlerMapping { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); 18 | 19 | private final Object[] basePackage; 20 | private final Map handlerExecutions; 21 | 22 | public AnnotationHandlerMapping(final Object... basePackage) { 23 | this.basePackage = basePackage; 24 | this.handlerExecutions = new HashMap<>(); 25 | } 26 | 27 | public void initialize() { 28 | final var controllerScanner = new ControllerScanner(basePackage); 29 | final var controllers = controllerScanner.getControllers(); 30 | final var methods = getRequestMappingMethods(controllers.keySet()); 31 | for (final var method : methods) { 32 | final var requestMapping = method.getAnnotation(RequestMapping.class); 33 | log.debug("register handlerExecution : url is {}, request method : {}, method is {}", requestMapping.value(), requestMapping.method(), method); 34 | addHandlerExecutions(controllers, method, requestMapping); 35 | } 36 | 37 | log.info("Initialized AnnotationHandlerMapping!"); 38 | } 39 | 40 | private void addHandlerExecutions(final Map, Object> controllers, final Method method, final RequestMapping rm) { 41 | final var handlerKeys = mapHandlerKeys(rm.value(), rm.method()); 42 | handlerKeys.forEach(handlerKey -> { 43 | handlerExecutions.put(handlerKey, new HandlerExecution(controllers.get(method.getDeclaringClass()), method)); 44 | }); 45 | } 46 | 47 | private List mapHandlerKeys(final String value, final RequestMethod[] originalMethods) { 48 | var targetMethods = originalMethods; 49 | if (targetMethods.length == 0) { 50 | targetMethods = RequestMethod.values(); 51 | } 52 | return Arrays.stream(targetMethods) 53 | .map(method -> new HandlerKey(value, method)) 54 | .collect(Collectors.toList()); 55 | } 56 | 57 | @SuppressWarnings("unchecked") 58 | private Set getRequestMappingMethods(final Set> controllers) { 59 | final var requestMappingMethods = new HashSet(); 60 | for (final var clazz : controllers) { 61 | requestMappingMethods 62 | .addAll(ReflectionUtils.getAllMethods(clazz, ReflectionUtils.withAnnotation(RequestMapping.class))); 63 | } 64 | return requestMappingMethods; 65 | } 66 | 67 | public Object getHandler(final HttpServletRequest request) { 68 | final var requestUri = request.getRequestURI(); 69 | final var requestMethod = RequestMethod.valueOf(request.getMethod().toUpperCase()); 70 | log.debug("requestUri : {}, requestMethod : {}", requestUri, requestMethod); 71 | return handlerExecutions.get(new HandlerKey(requestUri, requestMethod)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /study/src/test/java/connectionpool/stage1/Stage1Test.java: -------------------------------------------------------------------------------- 1 | package connectionpool.stage1; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import org.h2.jdbcx.JdbcConnectionPool; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.sql.SQLException; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class Stage1Test { 13 | 14 | private static final String H2_URL = "jdbc:h2:./test;DB_CLOSE_DELAY=-1"; 15 | private static final String USER = "sa"; 16 | private static final String PASSWORD = ""; 17 | 18 | /** 19 | * 커넥션 풀링(Connection Pooling)이란? 20 | * DataSource 객체를 통해 미리 커넥션(Connection)을 만들어 두는 것을 의미한다. 21 | * 새로운 커넥션을 생성하는 것은 많은 비용이 들기에 미리 커넥션을 만들어두면 성능상 이점이 있다. 22 | * 커넥션 풀링에 미리 만들어둔 커넥션은 재사용 가능하다. 23 | * 24 | * h2에서 제공하는 JdbcConnectionPool를 다뤄보며 커넥션 풀에 대한 감을 잡아보자. 25 | * 26 | * Connection Pooling and Statement Pooling 27 | * https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/javax/sql/package-summary.html 28 | */ 29 | @Test 30 | void testJdbcConnectionPool() throws SQLException { 31 | final JdbcConnectionPool jdbcConnectionPool = JdbcConnectionPool.create(H2_URL, USER, PASSWORD); 32 | 33 | assertThat(jdbcConnectionPool.getActiveConnections()).isZero(); 34 | try (final var connection = jdbcConnectionPool.getConnection()) { 35 | assertThat(connection.isValid(1)).isTrue(); 36 | assertThat(jdbcConnectionPool.getActiveConnections()).isEqualTo(1); 37 | } 38 | assertThat(jdbcConnectionPool.getActiveConnections()).isZero(); 39 | 40 | jdbcConnectionPool.dispose(); 41 | } 42 | 43 | /** 44 | * Spring Boot 2.0 부터 HikariCP를 기본 데이터 소스로 채택하고 있다. 45 | * https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#data.sql.datasource.connection-pool 46 | * Supported Connection Pools 47 | * We prefer HikariCP for its performance and concurrency. If HikariCP is available, we always choose it. 48 | * 49 | * HikariCP 공식 문서를 참고하여 HikariCP를 설정해보자. 50 | * https://github.com/brettwooldridge/HikariCP#rocket-initialization 51 | * 52 | * HikariCP 필수 설정 53 | * https://github.com/brettwooldridge/HikariCP#essentials 54 | * 55 | * HikariCP의 pool size는 몇으로 설정하는게 좋을까? 56 | * https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing 57 | * 58 | * HikariCP를 사용할 때 적용하면 좋은 MySQL 설정 59 | * https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration 60 | */ 61 | @Test 62 | void testHikariCP() { 63 | final var hikariConfig = new HikariConfig(); 64 | hikariConfig.setJdbcUrl(H2_URL); 65 | hikariConfig.setUsername(USER); 66 | hikariConfig.setPassword(PASSWORD); 67 | hikariConfig.setMaximumPoolSize(5); 68 | hikariConfig.addDataSourceProperty("cachePrepStmts", "true"); 69 | hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250"); 70 | hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); 71 | 72 | final var dataSource = new HikariDataSource(hikariConfig); 73 | final var properties = dataSource.getDataSourceProperties(); 74 | 75 | assertThat(dataSource.getMaximumPoolSize()).isEqualTo(5); 76 | assertThat(properties.getProperty("cachePrepStmts")).isEqualTo("true"); 77 | assertThat(properties.getProperty("prepStmtCacheSize")).isEqualTo("250"); 78 | assertThat(properties.getProperty("prepStmtCacheSqlLimit")).isEqualTo("2048"); 79 | 80 | dataSource.close(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | # Compiled class file 3 | *.class 4 | 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | ### JetBrains template 27 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 28 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 29 | 30 | .idea 31 | 32 | # User-specific stuff 33 | .idea/**/workspace.xml 34 | .idea/**/tasks.xml 35 | .idea/**/usage.statistics.xml 36 | .idea/**/dictionaries 37 | .idea/**/shelf 38 | 39 | # Generated files 40 | .idea/**/contentModel.xml 41 | 42 | # Sensitive or high-churn files 43 | .idea/**/dataSources/ 44 | .idea/**/dataSources.ids 45 | .idea/**/dataSources.local.xml 46 | .idea/**/sqlDataSources.xml 47 | .idea/**/dynamic.xml 48 | .idea/**/uiDesigner.xml 49 | .idea/**/dbnavigator.xml 50 | 51 | # Gradle 52 | .idea/**/gradle.xml 53 | .idea/**/libraries 54 | 55 | # Gradle and Maven with auto-import 56 | # When using Gradle or Maven with auto-import, you should exclude module files, 57 | # since they will be recreated, and may cause churn. Uncomment if using 58 | # auto-import. 59 | # .idea/artifacts 60 | # .idea/compiler.xml 61 | # .idea/jarRepositories.xml 62 | # .idea/modules.xml 63 | # .idea/*.iml 64 | # .idea/modules 65 | # *.iml 66 | # *.ipr 67 | 68 | # CMake 69 | cmake-build-*/ 70 | 71 | # Mongo Explorer plugin 72 | .idea/**/mongoSettings.xml 73 | 74 | # File-based project format 75 | *.iws 76 | 77 | # IntelliJ 78 | out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Cursive Clojure plugin 87 | .idea/replstate.xml 88 | 89 | # Crashlytics plugin (for Android Studio and IntelliJ) 90 | com_crashlytics_export_strings.xml 91 | crashlytics.properties 92 | crashlytics-build.properties 93 | fabric.properties 94 | 95 | # Editor-based Rest Client 96 | .idea/httpRequests 97 | 98 | # Android studio 3.1+ serialized cache file 99 | .idea/caches/build_file_checksums.ser 100 | 101 | ### Windows template 102 | # Windows thumbnail cache files 103 | Thumbs.db 104 | Thumbs.db:encryptable 105 | ehthumbs.db 106 | ehthumbs_vista.db 107 | 108 | # Dump file 109 | *.stackdump 110 | 111 | # Folder config file 112 | [Dd]esktop.ini 113 | 114 | # Recycle Bin used on file shares 115 | $RECYCLE.BIN/ 116 | 117 | # Windows Installer files 118 | *.cab 119 | *.msi 120 | *.msix 121 | *.msm 122 | *.msp 123 | 124 | # Windows shortcuts 125 | *.lnk 126 | 127 | ### Gradle template 128 | .gradle 129 | **/build/ 130 | !src/**/build/ 131 | 132 | # Ignore Gradle GUI config 133 | gradle-app.setting 134 | 135 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 136 | !gradle-wrapper.jar 137 | 138 | # Cache of project 139 | .gradletasknamecache 140 | 141 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 142 | # gradle/wrapper/gradle-wrapper.properties 143 | 144 | ### macOS template 145 | # General 146 | .DS_Store 147 | .AppleDouble 148 | .LSOverride 149 | 150 | # Icon must end with two \r 151 | Icon 152 | 153 | # Thumbnails 154 | ._* 155 | 156 | # Files that might appear in the root of a volume 157 | .DocumentRevisions-V100 158 | .fseventsd 159 | .Spotlight-V100 160 | .TemporaryItems 161 | .Trashes 162 | .VolumeIcon.icns 163 | .com.apple.timemachine.donotpresent 164 | 165 | # Directories potentially created on remote AFP share 166 | .AppleDB 167 | .AppleDesktop 168 | Network Trash Folder 169 | Temporary Items 170 | .apdisk 171 | 172 | tomcat.* 173 | tomcat.*/** 174 | 175 | **/WEB-INF/classes/** 176 | -------------------------------------------------------------------------------- /app/src/main/java/com/techcourse/dao/UserDao.java: -------------------------------------------------------------------------------- 1 | package com.techcourse.dao; 2 | 3 | import com.techcourse.domain.User; 4 | import com.interface21.jdbc.core.JdbcTemplate; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.sql.DataSource; 9 | import java.sql.Connection; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | import java.util.List; 14 | 15 | public class UserDao { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(UserDao.class); 18 | 19 | private final DataSource dataSource; 20 | 21 | public UserDao(final DataSource dataSource) { 22 | this.dataSource = dataSource; 23 | } 24 | 25 | public UserDao(final JdbcTemplate jdbcTemplate) { 26 | this.dataSource = null; 27 | } 28 | 29 | public void insert(final User user) { 30 | final var sql = "insert into users (account, password, email) values (?, ?, ?)"; 31 | 32 | Connection conn = null; 33 | PreparedStatement pstmt = null; 34 | try { 35 | conn = dataSource.getConnection(); 36 | pstmt = conn.prepareStatement(sql); 37 | 38 | log.debug("query : {}", sql); 39 | 40 | pstmt.setString(1, user.getAccount()); 41 | pstmt.setString(2, user.getPassword()); 42 | pstmt.setString(3, user.getEmail()); 43 | pstmt.executeUpdate(); 44 | } catch (SQLException e) { 45 | log.error(e.getMessage(), e); 46 | throw new RuntimeException(e); 47 | } finally { 48 | try { 49 | if (pstmt != null) { 50 | pstmt.close(); 51 | } 52 | } catch (SQLException ignored) {} 53 | 54 | try { 55 | if (conn != null) { 56 | conn.close(); 57 | } 58 | } catch (SQLException ignored) {} 59 | } 60 | } 61 | 62 | public void update(final User user) { 63 | // todo 64 | } 65 | 66 | public List findAll() { 67 | // todo 68 | return null; 69 | } 70 | 71 | public User findById(final Long id) { 72 | final var sql = "select id, account, password, email from users where id = ?"; 73 | 74 | Connection conn = null; 75 | PreparedStatement pstmt = null; 76 | ResultSet rs = null; 77 | try { 78 | conn = dataSource.getConnection(); 79 | pstmt = conn.prepareStatement(sql); 80 | pstmt.setLong(1, id); 81 | rs = pstmt.executeQuery(); 82 | 83 | log.debug("query : {}", sql); 84 | 85 | if (rs.next()) { 86 | return new User( 87 | rs.getLong(1), 88 | rs.getString(2), 89 | rs.getString(3), 90 | rs.getString(4)); 91 | } 92 | return null; 93 | } catch (SQLException e) { 94 | log.error(e.getMessage(), e); 95 | throw new RuntimeException(e); 96 | } finally { 97 | try { 98 | if (rs != null) { 99 | rs.close(); 100 | } 101 | } catch (SQLException ignored) {} 102 | 103 | try { 104 | if (pstmt != null) { 105 | pstmt.close(); 106 | } 107 | } catch (SQLException ignored) {} 108 | 109 | try { 110 | if (conn != null) { 111 | conn.close(); 112 | } 113 | } catch (SQLException ignored) {} 114 | } 115 | } 116 | 117 | public User findByAccount(final String account) { 118 | // todo 119 | return null; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/webapp/register.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | <%@ include file="include/header.jspf" %> 6 | 회원가입 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 회원 가입 17 | 18 | 19 | 20 | 21 | 아이디 22 | 23 | 24 | 25 | 이메일 주소 26 | 27 | 28 | 29 | 비밀번호 30 | 31 | 32 | 33 | 가입하기 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 63 | 64 | <%@ include file="include/footer.jspf" %> 65 | 66 | 67 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/jdbc/JdbcTemplate.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1.jdbc; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.sql.DataSource; 7 | import java.sql.Connection; 8 | import java.sql.PreparedStatement; 9 | import java.sql.SQLException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public class JdbcTemplate { 14 | private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); 15 | private final DataSource dataSource; 16 | 17 | public JdbcTemplate(final DataSource dataSource) { 18 | this.dataSource = dataSource; 19 | } 20 | 21 | public void update(final Connection connection, final String sql, final PreparedStatementSetter pss) throws DataAccessException { 22 | try (final var pstmt = connection.prepareStatement(sql)) { 23 | pss.setParameters(pstmt); 24 | pstmt.executeUpdate(); 25 | } catch (SQLException e) { 26 | throw new DataAccessException(e); 27 | } 28 | } 29 | 30 | public void update(final Connection connection, final String sql, final Object... parameters) { 31 | update(connection, sql, createPreparedStatementSetter(parameters)); 32 | } 33 | 34 | public void update(final String sql, final PreparedStatementSetter pss) throws DataAccessException { 35 | try (final var conn = dataSource.getConnection(); final var pstmt = conn.prepareStatement(sql)) { 36 | pss.setParameters(pstmt); 37 | pstmt.executeUpdate(); 38 | } catch (SQLException e) { 39 | throw new DataAccessException(e); 40 | } 41 | } 42 | 43 | public void update(final String sql, final Object... parameters) { 44 | update(sql, createPreparedStatementSetter(parameters)); 45 | } 46 | 47 | public void update(final PreparedStatementCreator psc, final KeyHolder holder) { 48 | try (final var conn = dataSource.getConnection(); final var ps = psc.createPreparedStatement(conn)) { 49 | ps.executeUpdate(); 50 | final var rs = ps.getGeneratedKeys(); 51 | if (rs.next()) { 52 | long generatedKey = rs.getLong(1); 53 | log.debug("Generated Key:{}", generatedKey); 54 | holder.setId(generatedKey); 55 | } 56 | rs.close(); 57 | } catch (SQLException e) { 58 | throw new DataAccessException(e); 59 | } 60 | } 61 | 62 | public T queryForObject(final Connection connection, final String sql, final RowMapper rm, final PreparedStatementSetter pss) { 63 | final var list = query(connection, sql, rm, pss); 64 | if (list.isEmpty()) { 65 | return null; 66 | } 67 | return list.get(0); 68 | } 69 | 70 | public T queryForObject(final Connection connection, final String sql, final RowMapper rm, final Object... parameters) { 71 | return queryForObject(connection, sql, rm, createPreparedStatementSetter(parameters)); 72 | } 73 | 74 | public List query(final Connection connection, final String sql, final RowMapper rm, final PreparedStatementSetter pss) throws DataAccessException { 75 | try (final var pstmt = connection.prepareStatement(sql)) { 76 | pss.setParameters(pstmt); 77 | return mapResultSetToObject(rm, pstmt); 78 | } catch (SQLException e) { 79 | throw new DataAccessException(e); 80 | } 81 | } 82 | 83 | private List mapResultSetToObject(final RowMapper rm, final PreparedStatement pstmt) { 84 | try (final var rs = pstmt.executeQuery()) { 85 | final var list = new ArrayList(); 86 | while (rs.next()) { 87 | list.add(rm.mapRow(rs)); 88 | } 89 | return list; 90 | } catch (SQLException e) { 91 | throw new DataAccessException(e); 92 | } 93 | } 94 | 95 | public List query(final Connection connection, final String sql, final RowMapper rm, final Object... parameters) { 96 | return query(connection, sql, rm, createPreparedStatementSetter(parameters)); 97 | } 98 | 99 | private PreparedStatementSetter createPreparedStatementSetter(final Object... parameters) { 100 | return pstmt -> { 101 | for (int i = 0; i < parameters.length; i++) { 102 | pstmt.setObject(i + 1, parameters[i]); 103 | } 104 | }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /study/src/test/java/connectionpool/PoolingVsNoPoolingTest.java: -------------------------------------------------------------------------------- 1 | package connectionpool; 2 | 3 | import com.mysql.cj.jdbc.MysqlDataSource; 4 | import com.zaxxer.hikari.HikariConfig; 5 | import com.zaxxer.hikari.HikariDataSource; 6 | import com.zaxxer.hikari.util.ClockSource; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.testcontainers.containers.MySQLContainer; 13 | import org.testcontainers.utility.DockerImageName; 14 | 15 | import javax.sql.DataSource; 16 | import java.sql.Connection; 17 | import java.sql.ResultSet; 18 | import java.sql.SQLException; 19 | import java.sql.Statement; 20 | 21 | /** 22 | * pooling을 사용한 경우와 사용하지 않은 경우 트래픽이 얼마나 차이나는지 확인해보자. 23 | * 24 | * network bandwidth capture 25 | * 터미널에 iftop를 설치하고 아래 명령어를 실행한 상태에서 테스트를 실행하자. 26 | * $ sudo iftop -i lo0 -nf "host localhost" 27 | * windows 사용자라면 wsl2를 사용하거나 다른 모니터링 툴을 찾아보자. 28 | */ 29 | class PoolingVsNoPoolingTest { 30 | 31 | private final Logger log = LoggerFactory.getLogger(PoolingVsNoPoolingTest.class); 32 | 33 | private static final int COUNT = 1000; 34 | 35 | private static MySQLContainer> container; 36 | 37 | @BeforeAll 38 | static void beforeAll() throws SQLException { 39 | // TestContainer로 임시 MySQL을 실행한다. 40 | container = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) 41 | .withDatabaseName("test"); 42 | container.start(); 43 | 44 | final var dataSource = createMysqlDataSource(); 45 | 46 | // 테스트에 사용할 users 테이블을 생성하고 데이터를 추가한다. 47 | try (Connection conn = dataSource.getConnection()) { 48 | conn.setAutoCommit(true); 49 | try (Statement stmt = conn.createStatement()) { 50 | stmt.execute("DROP TABLE IF EXISTS users;"); 51 | stmt.execute("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(100) NOT NULL) ENGINE=INNODB;"); 52 | stmt.executeUpdate("INSERT INTO users (email) VALUES ('hkkang@woowahan.com')"); 53 | conn.setAutoCommit(false); 54 | } 55 | } 56 | } 57 | 58 | @AfterAll 59 | static void afterAll() { 60 | container.stop(); 61 | } 62 | 63 | @Test 64 | void noPooling() throws SQLException { 65 | final var dataSource = createMysqlDataSource(); 66 | 67 | long start = ClockSource.currentTime(); 68 | connect(dataSource); 69 | long end = ClockSource.currentTime(); 70 | 71 | // 테스트 결과를 확인한다. 72 | log.info("Elapsed runtime: {}", ClockSource.elapsedDisplayString(start, end)); 73 | } 74 | 75 | @Test 76 | void pooling() throws SQLException { 77 | final var config = new HikariConfig(); 78 | config.setJdbcUrl(container.getJdbcUrl()); 79 | config.setUsername(container.getUsername()); 80 | config.setPassword(container.getPassword()); 81 | config.setMinimumIdle(1); 82 | config.setMaximumPoolSize(1); 83 | config.setConnectionTimeout(1000); 84 | config.setAutoCommit(false); 85 | config.setReadOnly(false); 86 | final var hikariDataSource = new HikariDataSource(config); 87 | 88 | long start = ClockSource.currentTime(); 89 | connect(hikariDataSource); 90 | long end = ClockSource.currentTime(); 91 | 92 | // 테스트 결과를 확인한다. 93 | log.info("Elapsed runtime: {}", ClockSource.elapsedDisplayString(start, end)); 94 | } 95 | 96 | private static void connect(DataSource dataSource) throws SQLException { 97 | // COUNT만큼 DB 연결을 수행한다. 98 | for (int i = 0; i < COUNT; i++) { 99 | try (Connection connection = dataSource.getConnection()) { 100 | try (Statement stmt = connection.createStatement(); 101 | ResultSet rs = stmt.executeQuery("SELECT * FROM users")) { 102 | if (rs.next()) { 103 | rs.getString(1).hashCode(); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | private static MysqlDataSource createMysqlDataSource() throws SQLException { 111 | final var dataSource = new MysqlDataSource(); 112 | dataSource.setUrl(container.getJdbcUrl()); 113 | dataSource.setUser(container.getUsername()); 114 | dataSource.setPassword(container.getPassword()); 115 | dataSource.setConnectTimeout(1000); 116 | return dataSource; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage2/Stage2Test.java: -------------------------------------------------------------------------------- 1 | package transaction.stage2; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | /** 14 | * 트랜잭션 전파(Transaction Propagation)란? 15 | * 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. 16 | * 17 | * FirstUserService 클래스의 메서드를 실행할 때 첫 번째 트랜잭션이 생성된다. 18 | * SecondUserService 클래스의 메서드를 실행할 때 두 번째 트랜잭션이 어떻게 되는지 관찰해보자. 19 | * 20 | * https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation 21 | */ 22 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 23 | class Stage2Test { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(Stage2Test.class); 26 | 27 | @Autowired 28 | private FirstUserService firstUserService; 29 | 30 | @Autowired 31 | private UserRepository userRepository; 32 | 33 | @AfterEach 34 | void tearDown() { 35 | userRepository.deleteAll(); 36 | } 37 | 38 | /** 39 | * 생성된 트랜잭션이 몇 개인가? 40 | * 왜 그런 결과가 나왔을까? 41 | */ 42 | @Test 43 | void testRequired() { 44 | final var actual = firstUserService.saveFirstTransactionWithRequired(); 45 | 46 | log.info("transactions : {}", actual); 47 | assertThat(actual) 48 | .hasSize(0) 49 | .containsExactly(""); 50 | } 51 | 52 | /** 53 | * 생성된 트랜잭션이 몇 개인가? 54 | * 왜 그런 결과가 나왔을까? 55 | */ 56 | @Test 57 | void testRequiredNew() { 58 | final var actual = firstUserService.saveFirstTransactionWithRequiredNew(); 59 | 60 | log.info("transactions : {}", actual); 61 | assertThat(actual) 62 | .hasSize(0) 63 | .containsExactly(""); 64 | } 65 | 66 | /** 67 | * firstUserService.saveAndExceptionWithRequiredNew()에서 강제로 예외를 발생시킨다. 68 | * REQUIRES_NEW 일 때 예외로 인한 롤백이 발생하면서 어떤 상황이 발생하는 지 확인해보자. 69 | */ 70 | @Test 71 | void testRequiredNewWithRollback() { 72 | assertThat(firstUserService.findAll()).hasSize(-1); 73 | 74 | assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) 75 | .isInstanceOf(RuntimeException.class); 76 | 77 | assertThat(firstUserService.findAll()).hasSize(-1); 78 | } 79 | 80 | /** 81 | * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. 82 | * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. 83 | */ 84 | @Test 85 | void testSupports() { 86 | final var actual = firstUserService.saveFirstTransactionWithSupports(); 87 | 88 | log.info("transactions : {}", actual); 89 | assertThat(actual) 90 | .hasSize(0) 91 | .containsExactly(""); 92 | } 93 | 94 | /** 95 | * FirstUserService.saveFirstTransactionWithMandatory() 메서드를 보면 @Transactional이 주석으로 되어 있다. 96 | * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. 97 | * SUPPORTS와 어떤 점이 다른지도 같이 챙겨보자. 98 | */ 99 | @Test 100 | void testMandatory() { 101 | final var actual = firstUserService.saveFirstTransactionWithMandatory(); 102 | 103 | log.info("transactions : {}", actual); 104 | assertThat(actual) 105 | .hasSize(0) 106 | .containsExactly(""); 107 | } 108 | 109 | /** 110 | * 아래 테스트는 몇 개의 물리적 트랜잭션이 동작할까? 111 | * FirstUserService.saveFirstTransactionWithNotSupported() 메서드의 @Transactional을 주석 처리하자. 112 | * 다시 테스트를 실행하면 몇 개의 물리적 트랜잭션이 동작할까? 113 | * 114 | * 스프링 공식 문서에서 물리적 트랜잭션과 논리적 트랜잭션의 차이점이 무엇인지 찾아보자. 115 | */ 116 | @Test 117 | void testNotSupported() { 118 | final var actual = firstUserService.saveFirstTransactionWithNotSupported(); 119 | 120 | log.info("transactions : {}", actual); 121 | assertThat(actual) 122 | .hasSize(0) 123 | .containsExactly(""); 124 | } 125 | 126 | /** 127 | * 아래 테스트는 왜 실패할까? 128 | * FirstUserService.saveFirstTransactionWithNested() 메서드의 @Transactional을 주석 처리하면 어떻게 될까? 129 | */ 130 | @Test 131 | void testNested() { 132 | final var actual = firstUserService.saveFirstTransactionWithNested(); 133 | 134 | log.info("transactions : {}", actual); 135 | assertThat(actual) 136 | .hasSize(0) 137 | .containsExactly(""); 138 | } 139 | 140 | /** 141 | * 마찬가지로 @Transactional을 주석처리하면서 관찰해보자. 142 | */ 143 | @Test 144 | void testNever() { 145 | final var actual = firstUserService.saveFirstTransactionWithNever(); 146 | 147 | log.info("transactions : {}", actual); 148 | assertThat(actual) 149 | .hasSize(0) 150 | .containsExactly(""); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" %> 2 | 3 | 4 | 5 | <%@ include file="include/header.jspf" %> 6 | 대시보드 7 | 8 | 9 | 10 | 11 | 대시보드 12 | 13 | 14 | <% if (session.getAttribute("user") != null) { %> 15 | 16 | 17 | 18 | 19 | 20 | 내정보 21 | 22 | 로그아웃 23 | 24 | 25 | 26 | 27 | <% } else { %> 28 | 29 | 로그인 30 | 31 | <% } %> 32 | 33 | 34 | 35 | 36 | 37 | 38 | Core 39 | 40 | 41 | 대시보드 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 대시보드 51 | 52 | 첫 페이지 53 | 54 | 55 | 56 | 57 | 58 | 59 | Bar Chart 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Pie Chart 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 90 | 91 | 92 | 93 | 94 | 95 | <%@ include file="include/footer.jspf" %> 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 대시보드 10 | 11 | 12 | 13 | 14 | 15 | 16 | 대시보드 17 | 18 | 19 | 20 | 21 | 22 | 로그인 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Core 46 | 47 | 48 | 대시보드 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 대시보드 58 | 59 | 첫 페이지 60 | 61 | 62 | 63 | 64 | 65 | 66 | Bar Chart 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Pie Chart 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /app/src/main/webapp/assets/img/error-404-monochrome.svg: -------------------------------------------------------------------------------- 1 | error-404-monochrome -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage2/FirstUserService.java: -------------------------------------------------------------------------------- 1 | package transaction.stage2; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Propagation; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import org.springframework.transaction.support.TransactionSynchronizationManager; 10 | 11 | import java.util.List; 12 | import java.util.Objects; 13 | import java.util.Set; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.Stream; 16 | 17 | @Service 18 | public class FirstUserService { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(FirstUserService.class); 21 | 22 | private final UserRepository userRepository; 23 | private final SecondUserService secondUserService; 24 | 25 | @Autowired 26 | public FirstUserService(final UserRepository userRepository, 27 | final SecondUserService secondUserService) { 28 | this.userRepository = userRepository; 29 | this.secondUserService = secondUserService; 30 | } 31 | 32 | @Transactional(readOnly = true, propagation = Propagation.REQUIRED) 33 | public List findAll() { 34 | return userRepository.findAll(); 35 | } 36 | 37 | @Transactional(propagation = Propagation.REQUIRED) 38 | public Set saveFirstTransactionWithRequired() { 39 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 40 | userRepository.save(User.createTest()); 41 | logActualTransactionActive(); 42 | 43 | final var secondTransactionName = secondUserService.saveSecondTransactionWithRequired(); 44 | 45 | return of(firstTransactionName, secondTransactionName); 46 | } 47 | 48 | @Transactional(propagation = Propagation.REQUIRED) 49 | public Set saveFirstTransactionWithRequiredNew() { 50 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 51 | userRepository.save(User.createTest()); 52 | logActualTransactionActive(); 53 | 54 | final var secondTransactionName = secondUserService.saveSecondTransactionWithRequiresNew(); 55 | 56 | return of(firstTransactionName, secondTransactionName); 57 | } 58 | 59 | @Transactional(propagation = Propagation.REQUIRED) 60 | public Set saveAndExceptionWithRequiredNew() { 61 | secondUserService.saveSecondTransactionWithRequiresNew(); 62 | 63 | userRepository.save(User.createTest()); 64 | logActualTransactionActive(); 65 | 66 | throw new RuntimeException(); 67 | } 68 | 69 | // @Transactional(propagation = Propagation.REQUIRED) 70 | public Set saveFirstTransactionWithSupports() { 71 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 72 | userRepository.save(User.createTest()); 73 | logActualTransactionActive(); 74 | 75 | final var secondTransactionName = secondUserService.saveSecondTransactionWithSupports(); 76 | 77 | return of(firstTransactionName, secondTransactionName); 78 | } 79 | 80 | // @Transactional(propagation = Propagation.REQUIRED) 81 | public Set saveFirstTransactionWithMandatory() { 82 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 83 | userRepository.save(User.createTest()); 84 | logActualTransactionActive(); 85 | 86 | final var secondTransactionName = secondUserService.saveSecondTransactionWithMandatory(); 87 | 88 | return of(firstTransactionName, secondTransactionName); 89 | } 90 | 91 | @Transactional(propagation = Propagation.REQUIRED) 92 | public Set saveFirstTransactionWithNotSupported() { 93 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 94 | userRepository.save(User.createTest()); 95 | logActualTransactionActive(); 96 | 97 | final var secondTransactionName = secondUserService.saveSecondTransactionWithNotSupported(); 98 | 99 | return of(firstTransactionName, secondTransactionName); 100 | } 101 | 102 | @Transactional(propagation = Propagation.REQUIRED) 103 | public Set saveFirstTransactionWithNested() { 104 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 105 | userRepository.save(User.createTest()); 106 | logActualTransactionActive(); 107 | 108 | final var secondTransactionName = secondUserService.saveSecondTransactionWithNested(); 109 | 110 | return of(firstTransactionName, secondTransactionName); 111 | } 112 | 113 | @Transactional(propagation = Propagation.REQUIRED) 114 | public Set saveFirstTransactionWithNever() { 115 | final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 116 | userRepository.save(User.createTest()); 117 | logActualTransactionActive(); 118 | 119 | final var secondTransactionName = secondUserService.saveSecondTransactionWithNever(); 120 | 121 | return of(firstTransactionName, secondTransactionName); 122 | } 123 | 124 | private Set of(final String firstTransactionName, final String secondTransactionName) { 125 | return Stream.of(firstTransactionName, secondTransactionName) 126 | .filter(transactionName -> !Objects.isNull(transactionName)) 127 | .collect(Collectors.toSet()); 128 | } 129 | 130 | private void logActualTransactionActive() { 131 | final var currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); 132 | final var actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); 133 | final var emoji = actualTransactionActive ? "✅" : "❌"; 134 | log.info("\n{} is Actual Transaction Active : {} {}", currentTransactionName, emoji, actualTransactionActive); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /study/src/test/java/transaction/stage1/Stage1Test.java: -------------------------------------------------------------------------------- 1 | package transaction.stage1; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import org.h2.jdbcx.JdbcDataSource; 6 | import org.junit.jupiter.api.Test; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.testcontainers.containers.JdbcDatabaseContainer; 10 | import org.testcontainers.containers.MySQLContainer; 11 | import org.testcontainers.containers.output.Slf4jLogConsumer; 12 | import org.testcontainers.utility.DockerImageName; 13 | import transaction.DatabasePopulatorUtils; 14 | import transaction.RunnableWrapper; 15 | 16 | import javax.sql.DataSource; 17 | import java.sql.Connection; 18 | import java.sql.SQLException; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | /** 24 | * 격리 레벨(Isolation Level)에 따라 여러 사용자가 동시에 db에 접근했을 때 어떤 문제가 발생하는지 확인해보자. 25 | * ❗phantom reads는 docker를 실행한 상태에서 테스트를 실행한다. 26 | * ❗phantom reads는 MySQL로 확인한다. H2 데이터베이스에서는 발생하지 않는다. 27 | * 28 | * 참고 링크 29 | * https://en.wikipedia.org/wiki/Isolation_(database_systems) 30 | * 31 | * 각 테스트에서 어떤 현상이 발생하는지 직접 경험해보고 아래 표를 채워보자. 32 | * + : 발생 33 | * - : 발생하지 않음 34 | * Read phenomena | Dirty reads | Non-repeatable reads | Phantom reads 35 | * Isolation level | | | 36 | * -----------------|-------------|----------------------|-------------- 37 | * Read Uncommitted | | | 38 | * Read Committed | | | 39 | * Repeatable Read | | | 40 | * Serializable | | | 41 | */ 42 | class Stage1Test { 43 | 44 | private static final Logger log = LoggerFactory.getLogger(Stage1Test.class); 45 | private DataSource dataSource; 46 | private UserDao userDao; 47 | 48 | private void setUp(final DataSource dataSource) { 49 | this.dataSource = dataSource; 50 | DatabasePopulatorUtils.execute(dataSource); 51 | this.userDao = new UserDao(dataSource); 52 | } 53 | 54 | /** 55 | * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. 56 | * + : 발생 57 | * - : 발생하지 않음 58 | * Read phenomena | Dirty reads 59 | * Isolation level | 60 | * -----------------|------------- 61 | * Read Uncommitted | 62 | * Read Committed | 63 | * Repeatable Read | 64 | * Serializable | 65 | */ 66 | @Test 67 | void dirtyReading() throws SQLException { 68 | setUp(createH2DataSource()); 69 | 70 | // db에 새로운 연결(사용자A)을 받아와서 71 | final var connection = dataSource.getConnection(); 72 | 73 | // 트랜잭션을 시작한다. 74 | connection.setAutoCommit(false); 75 | 76 | // db에 데이터를 추가하고 커밋하기 전에 77 | userDao.insert(connection, new User("gugu", "password", "hkkang@woowahan.com")); 78 | 79 | new Thread(RunnableWrapper.accept(() -> { 80 | // db에 connection(사용자A)이 아닌 새로운 연결인 subConnection(사용자B)을 받아온다. 81 | final var subConnection = dataSource.getConnection(); 82 | 83 | // 적절한 격리 레벨을 찾는다. 84 | final int isolationLevel = Connection.TRANSACTION_NONE; 85 | 86 | // 트랜잭션 격리 레벨을 설정한다. 87 | subConnection.setTransactionIsolation(isolationLevel); 88 | 89 | // ❗️gugu 객체는 connection에서 아직 커밋하지 않은 상태다. 90 | // 격리 레벨에 따라 커밋하지 않은 gugu 객체를 조회할 수 있다. 91 | // 사용자B가 사용자A가 커밋하지 않은 데이터를 조회하는게 적절할까? 92 | final var actual = userDao.findByAccount(subConnection, "gugu"); 93 | 94 | // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. 95 | // 어떤 격리 레벨일 때 다른 연결의 커밋 전 데이터를 조회할 수 있을지 찾아보자. 96 | // 다른 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자. 97 | log.info("isolation level : {}, user : {}", isolationLevel, actual); 98 | assertThat(actual).isNull(); 99 | })).start(); 100 | 101 | sleep(0.5); 102 | 103 | // 롤백하면 사용자A의 user 데이터를 저장하지 않았는데 사용자B는 user 데이터가 있다고 인지한 상황이 된다. 104 | connection.rollback(); 105 | } 106 | 107 | /** 108 | * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. 109 | * + : 발생 110 | * - : 발생하지 않음 111 | * Read phenomena | Non-repeatable reads 112 | * Isolation level | 113 | * -----------------|--------------------- 114 | * Read Uncommitted | 115 | * Read Committed | 116 | * Repeatable Read | 117 | * Serializable | 118 | */ 119 | @Test 120 | void noneRepeatable() throws SQLException { 121 | setUp(createH2DataSource()); 122 | 123 | // 테스트 전에 필요한 데이터를 추가한다. 124 | userDao.insert(dataSource.getConnection(), new User("gugu", "password", "hkkang@woowahan.com")); 125 | 126 | // db에 새로운 연결(사용자A)을 받아와서 127 | final var connection = dataSource.getConnection(); 128 | 129 | // 트랜잭션을 시작한다. 130 | connection.setAutoCommit(false); 131 | 132 | // 적절한 격리 레벨을 찾는다. 133 | final int isolationLevel = Connection.TRANSACTION_NONE; 134 | 135 | // 트랜잭션 격리 레벨을 설정한다. 136 | connection.setTransactionIsolation(isolationLevel); 137 | 138 | // 사용자A가 gugu 객체를 조회했다. 139 | final var user = userDao.findByAccount(connection, "gugu"); 140 | log.info("user : {}", user); 141 | 142 | new Thread(RunnableWrapper.accept(() -> { 143 | // 사용자B가 새로 연결하여 144 | final var subConnection = dataSource.getConnection(); 145 | 146 | // 사용자A가 조회한 gugu 객체를 사용자B가 다시 조회했다. 147 | final var anotherUser = userDao.findByAccount(subConnection, "gugu"); 148 | 149 | // ❗사용자B가 gugu 객체의 비밀번호를 변경했다.(subConnection은 auto commit 상태) 150 | anotherUser.changePassword("qqqq"); 151 | userDao.update(subConnection, anotherUser); 152 | })).start(); 153 | 154 | sleep(0.5); 155 | 156 | // 사용자A가 다시 gugu 객체를 조회했다. 157 | // 사용자B는 패스워드를 변경하고 아직 커밋하지 않았다. 158 | final var actual = userDao.findByAccount(connection, "gugu"); 159 | 160 | // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. 161 | // 각 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자. 162 | log.info("isolation level : {}, user : {}", isolationLevel, actual); 163 | assertThat(actual.getPassword()).isEqualTo("password"); 164 | 165 | connection.rollback(); 166 | } 167 | 168 | /** 169 | * phantom read는 h2에서 발생하지 않는다. mysql로 확인해보자. 170 | * 격리 수준에 따라 어떤 현상이 발생하는지 테스트를 돌려 직접 눈으로 확인하고 표를 채워보자. 171 | * + : 발생 172 | * - : 발생하지 않음 173 | * Read phenomena | Phantom reads 174 | * Isolation level | 175 | * -----------------|-------------- 176 | * Read Uncommitted | 177 | * Read Committed | 178 | * Repeatable Read | 179 | * Serializable | 180 | */ 181 | @Test 182 | void phantomReading() throws SQLException { 183 | 184 | // testcontainer로 docker를 실행해서 mysql에 연결한다. 185 | final var mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) 186 | .withLogConsumer(new Slf4jLogConsumer(log)); 187 | mysql.start(); 188 | setUp(createMySQLDataSource(mysql)); 189 | 190 | // 테스트 전에 필요한 데이터를 추가한다. 191 | userDao.insert(dataSource.getConnection(), new User("gugu", "password", "hkkang@woowahan.com")); 192 | 193 | // db에 새로운 연결(사용자A)을 받아와서 194 | final var connection = dataSource.getConnection(); 195 | 196 | // 트랜잭션을 시작한다. 197 | connection.setAutoCommit(false); 198 | 199 | // 적절한 격리 레벨을 찾는다. 200 | final int isolationLevel = Connection.TRANSACTION_NONE; 201 | 202 | // 트랜잭션 격리 레벨을 설정한다. 203 | connection.setTransactionIsolation(isolationLevel); 204 | 205 | // 사용자A가 id로 범위를 조회했다. 206 | userDao.findGreaterThan(connection, 1); 207 | 208 | new Thread(RunnableWrapper.accept(() -> { 209 | // 사용자B가 새로 연결하여 210 | final var subConnection = dataSource.getConnection(); 211 | 212 | // 트랜잭션 시작 213 | subConnection.setAutoCommit(false); 214 | 215 | // 새로운 user 객체를 저장했다. 216 | // id는 2로 저장된다. 217 | userDao.insert(subConnection, new User("bird", "password", "bird@woowahan.com")); 218 | 219 | subConnection.commit(); 220 | })).start(); 221 | 222 | sleep(0.5); 223 | 224 | // MySQL에서 팬텀 읽기를 시연하려면 update를 실행해야 한다. 225 | // http://stackoverflow.com/questions/42794425/unable-to-produce-a-phantom-read/42796969#42796969 226 | userDao.updatePasswordGreaterThan(connection, "qqqq", 1); 227 | 228 | // 사용자A가 다시 id로 범위를 조회했다. 229 | final var actual = userDao.findGreaterThan(connection, 1); 230 | 231 | // 트랜잭션 격리 레벨에 따라 아래 테스트가 통과한다. 232 | // 각 격리 레벨은 어떤 결과가 나오는지 직접 확인해보자. 233 | log.info("isolation level : {}, user : {}", isolationLevel, actual); 234 | assertThat(actual).hasSize(1); 235 | 236 | connection.rollback(); 237 | mysql.close(); 238 | } 239 | 240 | private static DataSource createMySQLDataSource(final JdbcDatabaseContainer> container) { 241 | final var config = new HikariConfig(); 242 | config.setJdbcUrl(container.getJdbcUrl()); 243 | config.setUsername(container.getUsername()); 244 | config.setPassword(container.getPassword()); 245 | config.setDriverClassName(container.getDriverClassName()); 246 | return new HikariDataSource(config); 247 | } 248 | 249 | private static DataSource createH2DataSource() { 250 | final var jdbcDataSource = new JdbcDataSource(); 251 | // h2 로그를 확인하고 싶을 때 사용 252 | // jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_SYSTEM_OUT=3;MODE=MYSQL"); 253 | jdbcDataSource.setUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=MYSQL;"); 254 | jdbcDataSource.setUser("sa"); 255 | jdbcDataSource.setPassword(""); 256 | return jdbcDataSource; 257 | } 258 | 259 | private void sleep(double seconds) { 260 | try { 261 | TimeUnit.MILLISECONDS.sleep((long) (seconds * 1000)); 262 | } catch (InterruptedException ignored) { 263 | } 264 | } 265 | } 266 | --------------------------------------------------------------------------------
Internal Server Error
This requested URL was not found on this server.
Unauthorized
Access to this resource is denied.