-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[5주차] 수지, 지웅, 건우 #5
Comments
[Chapter 15] 간단한 웹 어플리케이션의 구조1 간단한 웹 어플리케이션의 구성요소
2 서비스의 구현
3 컨트롤러에서의 DAO 접근
public class MemberService{
...
public Member getMember(Long id){
return memberDao.selectById(id);
}
} 4 패키지 구성
[Chapter 16] JSON 응답과 요청 처리
1 JSON 개요
2 Jackson 의존 설정
public class Person { <=> { "name":"이름",
private String name; "age":10
private int age; }
..get/set 메서드
} 3 @RestController로 JSON 형식 응답
3.1 @JsonIgnore를 이용한 제외 처리
import com.fasterxml.jackson.annotation.JsonIgnore;
public class Member{
private Long id;
private String email;
@JsonIgnore;
private String password;
private String name;
private LocalDateTime registerDateTime;
} 3.2 날짜 형식 변환 처리: @jsonformat 사용
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
public class Member{
private Long id;
private String email;
private String name;
@JsonFormat(shape = Shape.STRING)
private LocalDateTime registerDateTime;
} //형식을 원하는 형식으로 변환하고싶다면
@JsonFormat(pattern = "yyyyMMddHHmmss" 3.3 날짜 형식 변환 처리 : 기본 적용 설정
3.4 응답 데이터의 컨텐츠 형식
자바객체 ⇒ JSON JSON ⇒ 자바객체 4 @RequestBody로 JSON 요청 처리
**import org.springframework.web.bind.annotation.RequestBody;**
...
@RestController
public class RestMemberController{
...
@PostMapping("/api/members")
public void newMember(
**@RequestBody** @valid RegisterRequest regReq,
HttpServletResponse response) throws IOException{
...
}
}
} 4.1 JSON 데이터의 날짜 형식 다루기
//특정 패턴을 가진 문자열을 변환하고자 할 때
@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime birthDateTime;
@JsonFormat(pattern = "yyyyMMdd HHmmss")
private Date birthDate;
...
public class MvcConfig implements VecMvcConfigurer{
...
@Override
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters){
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
**.featuresToDisable(SerializationFeature.INDENT_OUTPUT)
.deserailizerByType(LocalDateTime.class,
new LocalDateTimeeserializer(formatter))
.simpleDateFormat("yyyyMMdd HHmmss")
.build();
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
}
4.2 요청 객체 검증하기
PostMapping("/api/members")
public void newMember(
@RequestBody RegisterRequest regRea, Errors errors,
HttpServletResponse response) throws IOException {
try {
new RegisterRequestValidator) validate(regRea, errors);
if (errors.hasErrors()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
} ...
} catch (DuplicateMemberException dupEx) {
response.sendError(HttpServletResponse.SC_CONFLICT);
}
} 5 ResponseEntiry로 객체 리턴하고 응답 코드 지정하기
5.1 ResponseEntity를 이용한 응답 데이터 처리
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@RestController
public class RestMemberController {
private MemberDao memberDao;
.. 생략
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member @PathVariable Long id) {
Member member = memberDao.selectByld (id);
if (member == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
return ResponseEntity.status(HttpStatus.OK).body(member);
}
5.2 @ExceptionHandler 적용 메서드에서 ResponseEntity로 응답하기
⇒ @ExceptionHandler 애노테이션을 적용한 메서드에서 에러 응답을 처리 @GetMapping("/api/members/{id}")
public ResponseEntity<Object> member @PathVariable Long id) {
Member member = memberDao.selectByld (id);
if (member == null) {
throw new MemberNotFoundException();
}
return member;
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
5.3 @Valid 에러 결과를 JSON으로 응답하기
[Chapter 17] 프로필과 프로퍼티 파일1 프로필
1.1 @configuration 설정에서 프로필 사용하기
//DsDevConfig 클래스를 설정으로 사용
@Configuration
@Profile("dev")
public class DsDevConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
//"dev"가 아닌 "real" 프로필 활성화 했을 대
@Configuration
@Profile("real")
public class DsRealConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
1.2 @configuration을 이용한 프로필 설정
주의) 중첩된 @configuration 설정 사용할 때 중첩 클래스는 static이어야 함 @Configuration
public class MemberConfigWithProfile{
@Autowired
private DataSource dataSource;
@Bean
public MemberDao memberDao(){
return new MemberDao(dataSource);
}
@Configuration
@Profile("dev")
public class DsDevConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
@Configuration
@Profile("real")
public class DsRealConfig{
@Bean(destroyMethod="close")
public DataSource dataSource(){
...
}
}
} 1.3 다수 프로필 설정
//real, test 프로필 모두 사용
@Configuration
@Profile(”real,test”)
public class DataSourceJndiConfig{ ... )
//real 프로필 비활성화
@Configuration
@Profile(”!real”)
public class DataSourceJndiConfig{ ... ) 1.4 어플리케이션에서 프로필 설정하기
<servlet>
...
<init-param>
<param-name>spring.profiles.active</param-name>
<param-value>dev</param-value>
</init-param>
...
</servlet> 2 프로퍼티 파일을 이용한 프로퍼티 설정
2.1 @configuration 애노테이션 이용 자바 설정에서의 프로퍼티 사용
2.2 빈 클래스에서 사용하기
package spring;
import ...;
public class Info{
@Value("${Info.version}")
private String version;
...
}
public class Info{
private String version;
...
@Value("${info.version}")
public void setVersion(String version){
this.version=version;
}
} |
Chapter 10 : 스프링 MVC 프레임워크 동작 방식1. 스프링 MVC 핵심 구성 요소위 사진은 스프링 MVC의 핵심 구성요소를 보여주는 그림입니다. 중앙에 위치한 DispatcherServlet 은 모든 연결을 담당합니다. (1) 웹 브라우저로부터 요청이 들어오면 디스패쳐 서블릿은 그 요청을 처리하기 위한 컨트롤러 객체를 탐색하게 됩니다. (2) 이때 디스패쳐 서블릿은 직접 컨트롤러를 검색하지않고, HandlerMapping 이라는 빈 객체에게 컨트롤러 검색을 요청합니다. 핸들러매핑은 클라이언트의 요청 경로를 이용해서 이를 처리할 컨트롤러 빈 객체를 디스패쳐 서블릿에게 전달합니다. (3) 디스패쳐 서블릿은 핸들러 매핑이 찾아준 컨트롤러 객체를 처리할 수 있는 HandlerAdapter 빈에게 요청 처리를 위임합니다. (4~5) 핸들러 어댑터는 컨트롤러의 알맞은 메소드를 호출해서 요청을 처리합니다. (6) 이후, 그 결과를 디스패쳐 서블릿에게 리턴합니다. (7) 핸들러 어댑터로부터 컨트롤러의 요청 처리 결과를 ModelAndView로 받으면 디스패쳐서블릿은 결과를 보여줄 뷰를 찾기 위해 ViewResolver 빈 객체를 사용합니다. ModelAndView는 컨트롤러가 리턴한 뷰 이름을 담고 있는데 ViewResolver는 이 뷰 이름에 해당하는 View 객체를 찾거나 생성해서 리턴합니다. (8) 디스패쳐서블릿은 뷰 리졸버가 리턴한 View 객체에게 응답 결과 생성을 요청합니다. (9) 이후 웹 브라우저에 전송할 응답결과를 생성하고 모든 과정이 끝이 납니다. 2. @controller를 위한 HandlerMapping과 HandlerAdapter@controller 적용 객체는 디스패쳐 서블릿 입장에서 보면 한 종류의 핸들러 객체입니다. RequestMappingHandlerMapping은 @controller 어노테이션이 적용된 객체의 요청 매핑 어노테이션(@GetMapping) 값을 이용해 웹 브라우저의 요청을 처리할 컨트롤러 빈을 찾습니다.
RequestMappingHandlerAdapter는 컨트롤러 메소드 결과 값이 String 타입이면 해당 값을 뷰 이름으로 갖는 ModelAndView 객체를 생성해서 디스패쳐 서블릿에게 리턴하게 됩니다. 위 코드에서는 "hello"를 리턴하므로 view 이름으로 "hello"를 사용합니다. Chapter 11 : MVC 1 : 요청 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델1. 요청 매핑 어노테이션을 이용한 경로 매핑웹 어플리케이션을 개발하는 것은 다음 코드를 작성하는 것입니다.
특정 요청 URL을 처리하는 코드는 @controller 어노테이션을 사용한 컨트롤러 클래스를 이용해 구현합니다.
앞서 본 코드가 @GetMapping 어노테이션을 사용하여 "/hello" 요청 경로를 hello() 메소드가 처리하도록 설정한 코드입니다. 요청 매핑 어노테이션을 적용한 메소드를 두 개 이상 정의할 수도 있습니다. 이렇게 여러 단계를 거쳐 하나의 기능이 완성되는 경우, 관련 요청 경로를 한 개의 컨트롤러 클래스에서 처리하면 코드 관리에 도움이 됩니다.
위 코드를 보면 각 요청 매핑 어노테이션의 경로가 "/register" 로 똑같이 시작합니다. 이런 경우 다음 코드처럼 공통되는 부분의 경로를 담은 @RequestMapping 어노테이션을 클래스에 적용하고 각 메소드는 나머지 경로를 값으로 갖는 요청 어노테이션을 적용할 수 있습니다.
스프링 MVC는 클래스에 적용한 요청 매핑 어노테이션의 경로와 메소드에 적용한 요청 매핑 어노테이션의 경로를 합쳐서 2. GET과 POST의 구분HTML 폼 코드에서
라고 돼있다고 해봅시다.
그렇다면 폼을 전송할 때 HTTP 메소드 중 POST 방식을 사용한다는 것인데, 스프링 MVC는 별도 설정이 없으면 GET, POST 방식에 상관없이 @RequestMapping 에 지정한 경로와 일치하는 요청을 처리합니다. 만약 POST 방식 요청만 처리하고 싶다면 다음과 같이 @PostMapping 어노테이션을 사용하여 제한할 수 있습니다.
위 코드처럼 설정하면 handleStep2 메소드는 POST 방식의 /register/step2 요청 경로만 처리하며 같은 요청 경로의 GET 요청은 처리하지 않게 됩니다. 마찬가지로 @GetMapping 을 사용해도 같습니다. 3. 요청 파라미터 접근
위 코드를 보면 다음처럼 약관에 동의할 경우 값이 true 인 'agree' 요청 파라미터의 값을 POST 방식으로 전송합니다.
컨트롤러 메소드에서 요청 파라미터를 사용하는 첫번째 방법은 HttpServletRequest 를 직접 이용하는 것입니다.
요청 파라미터에 접근하는 두번째 방법은 @RequestParam 어노테이션을 이용하는 것입니다. handleStep2() 메소드는 agree 요청에 따라 파라미터의 값이 true가 아니면 다시 약관 동의 폼을 보여주기 위해 "register/step1" 뷰 이름을 리턴합니다. 약관에 동의 했다면 입력 폼을 보여주기 위해 "register/step2"를 뷰 이름으로 리턴할 것입니다. 4. 리다이렉트 처리웹 브라우저에서 http://localhost:8080/register/step2 주소로 직접 입력하면, 잘못된 전송 방식으로 요청이 왔을 때 에러화면 보다 알맞은 경로로 리다이렉트 하는 것이 더 좋을 때가 있습니다.
위 형태처럼 뷰 이름을 리턴하면 됩니다.
위 handleStep2Get() 메소드를 통해 리다이렉트를 할 수 있습니다. 5. 커맨드 객체를 이용해 요청 파리미터 사용만약 폼이 eamil, name, password, confirmPassword 정보를 파라민터로 서버에 전송한다고 해봅시다. 그렇다면 폼 전송 요청을 처리하는 컨트롤러 코드는 각 파라미터의 값을 구하기 위해 다음과 같은 코드를 작성해야 합니다.
위 코드는 올바르게 동작하지만, 요청 파라미터 개수가 증가할 때 마다 메소드의 코드 길이가 비례하게 늘어날 것 입니다. 스프링은 이러한 불편함을 줄이기 위해 요청 파라미터의 값을 커맨드 객체에 담아주는 기능을 제공합니다.
RegisterRequest 클래스 안에는 setEmail(), setName(), setPassword(), setConfirmPassword() 메소드가 있습니다. 여기서 사용한 @Getter , @Setter는 롬복 라이브러리에서 끌어다 온 어노테이션입니다. 스프링은 이 메소드들을 사용해서 email,name,password,confirmPassword 요청 파라미터의 값을 커맨드 객체에 복사한 뒤 regReg 파라미터로 전달하게 됩니다.
6. @ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경커맨드 객체에 접근할 때 사용할 속성 이름을 변경하고 싶다면 파라미터에 @ModelAttribute 어노테이션을 적용하면 됩니다.
@ModelAttribute 어노테이션은 모델에서 사용할 속성 이름을 값으로 설정합니다. 7. 커맨드 객체와 스프링 폼 연동스프링 MVC가 제공하는 커스텀 태그를 사용하면 좀 더 간단하게 커맨드 객체의 값을 출력할 수 있습니다. 스프링은 form:form 태그와 form:imput 태그를 제공합니다.
form:form 태그를 사용하려면 커맨드 객체가 존재해야 합니다. 8. 주요 에러 발생 상황404 에러?요청 경로를 처리할 컨트롤러가 존재하지 않거나 WebMvcConfigurer를 이용한 설정이 없다면 발생합니다. 404 에러가 발생한다면 다음 사항을 확인해야 합니다.
405 에러?지원하지 않는 전송 방식을 사용한 경우 발생합니다. 예를 들면, POST 방식만 처리하는 요청 경로를 GET 방식으로 연결하는 경우가 있습니다. 9. Model을 통해 컨트롤러에서 뷰 데이터 전달컨트롤러는 뷰가 응답 화면을 구성하는데 필요한 데이터를 생성해서 전달해야 한다.
뷰에 데이터를 전달하는 컨트롤러는 위 두가지를 하면 됩니다. addAtribute() 메소드의 첫번째 파라미터는 속성 이름입니다. ModelAndView를 활용하면 Model을 통해 뷰에 전달할 데이터 설정, 결과를 보여줄 뷰 이름을 리턴하는 것을 한번에 처리할 수 있습니다. 요청 매핑 어노테이션을 적용한 메소드는 String 타입 대신 ModelAndView 를 리턴할 수 있습니다.
뷰에 전달할 모델 데이터는 addObject() 메소드로 추가하고, 뷰이름은 setViewNamee() 메소드를 이용해 지정합니다. |
Chapter 08: DB 연동1. JDBC 프로그래밍의 단점을 보완하는 스프링
JDBC API를 이용하면 DB 연동에 필요한 Connection을 구한 다음 쿼리를 실행하기 위한 PreparedStatement를 생성함. 쿼리를 실행한 뒤에는 finally 블록에서 ResultSet, PreparedStatement, Connection을 닫음. 점선으로 표시한 부분은 데이터 처리와 상관없는 코드지만, JDBC 프로그래밍 시 구조적으로 반복됨. 템플릿 메서드 패턴과 전략 패턴을 활용한 해결 스프링은 두 패턴을 엮은 JdbcTemplate 클래스를 제공함. 이 클래스를 사용하면 중복된 코드를 줄일 수 있음.
자바 8의 람다를 사용하면 코드를 더 줄일 수 있음.
스프링이 제공하는 또 다른 장점 트랜잭션 관리가 쉬움. JDBC API로 트랜잭션을 처리하려면 Connection의 setAutoCommit(false)을 이용해서 자동 커밋을 비활성화하고 commit()과 rollback() 메서드를 이용해서 트랜잭션을 커밋하거나 롤백해야 함.
스프링을 사용하면 트랜잭션을 적용하고 싶은 메서드에 @transactional 애노테이션을 붙이기만 하면 됨.
커밋과 롤백 처리는 스프링이 알아서 처리하므로 코드를 작성하는 사람은 트랜잭션 처리를 제외한 핵심 코드만 집중해서 작성하면 됨. 2. 프로젝트 준비이 장에서 사용할 예제 코드 대부분은 앞서 작성했던 3장에서 가져올 것임. 3장 예제를 작성하지 않았다면 책에서 제공하는 소스 코드를 다운로드한 뒤에 따라 하면 됨. 2.1 프로젝트 생성8장을 위한 메이븐 프로젝트를 생성함.
2.2 DB 테이블 생성MySQL에 root 사용자로 연결한 뒤 아래 쿼리를 실행하여 DB 사용자, 데이터베이스, 테이블을 생성함.
3. DataSource 설정JDBC API는 DriverManager 외에 DataSource를 이용해서 DB 연결을 구하는 방법을 정의하고 있음.
스프링이 제공하는 DB 연동 기능은 DataSource를 사용해서 DB Connection을 구함. 3.1 Tomcat JDBC의 주요 프로퍼티Tomcat JDBC 모듈의 org.apache.tomcat.jdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스임. 설정 메서드 설명
1. JDBC 프로그래밍의 단점을 보완하는 스프링JDBC 프로그래밍은 보통 이런식으로 코드를 짠다고 합니다.
JDBC API를 이용하면 DB 연동에 필요한 Connection을 구한 다음 쿼리를 실행하기 위한 PreparedStatement를 생성함.
자바 8의 람다를 사용하면 코드를 더 줄일 수 있음.
JDBC API로 트랜잭션을 처리하려면 Connection의 setAutoCommit(false)을 이용해서 자동 커밋을 비활성화하고
커밋과 롤백 처리는 스프링이 알아서 처리하므로 코드를 작성하는 사람은 트랜잭션 처리를 제외한 핵심 코드만 집중해서 작성하면 됨. 2. 프로젝트 준비이 장에서 사용할 예제 코드 대부분은 앞서 작성했던 3장에서 가져올 것임. 2.1 프로젝트 생성8장에서 예시를 위한 메이븐 프로젝트를 생성함.
2.2 DB 테이블 생성DBMS로 MySQL을 사용함.
3. DataSource 설정JDBC API는 DriverManager 외에 DataSource를 이용해서 DB 연결을 구하는 방법을 정의하고 있음.
스프링이 제공하는 DB 연동 기능은 DataSource를 사용해서 DB Connection을 구함.
3.1 Tomcat JDBC의 주요 프로퍼티Tomcat JDBC 모듈의 org.apache.tomcat.jdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스임. 설정 메서드 설명
4. JdbcTemplate을 이용한 쿼리 실행스프링을 사용하면 DataSource나 Connection, Statement, ResultSet을 직접 사용하지 않고 JdbcTemplate을 이용해서 편리하게 쿼리를 실행할 수 있음. 4.1 JdbcTemplate 생성하기가장 먼저 해야 할 작업은 JdbcTemplate 객체를 생성하는 것임. ``java import javax.sql.DataSource; public class MemberDao {
} JdbcTemplate 객체를 생성하려면 DataSource를 생성자에 전달하면 됨. 이를 위해 DataSource를 주입받도록 MemberDao 클래스의 생성자를 구현함. 다음과 같이 설정 메서드 방식을 이용해서 DataSource를 주입받고 JdbcTemplate을 생성할 수도 있음. public class MemberDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
}
``
스프링 설정에 MemberDao 빈 설정을 추가함.
``java
@Configuration
public class AppCtx {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
ds.setUsername("spring5");
ds.setPassword("spring5");
ds.setInitialSize(2);
ds.setMaxActive(10);
return ds;
}
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
}
``
## 4.2 JdbcTemplate을 이용한 조회 쿼리 실행
JdbcTemplate 클래스는 SELECT 쿼리 실행을 위한 query() 메서드를 제공함. 자주 사용되는 쿼리 메서드는 다음과 같음.
List<T> query(String sql, RowMapper<T> rowMapper)
List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)
List<T> query(String sql, RowMapper<T> rowMapper, Object... args)
query() 메서드는 sql 파라미터로 전달받은 쿼리를 실행하고 RowMapper를 이용해서 ResultSet의 결과를 자바 객체로 변환함.
``java
package spring;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Member selectByEmail(String email) {
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
},
email
);
return results.isEmpty() ? null : results.get(0);
}
}
``
JdbcTemplate의 query() 메서드를 이용해서 쿼리를 실행함.
쿼리는 인덱스 파라미터(물음표)를 포함하고 있음.
인덱스 파라미터에 들어갈 값은 query() 메서드의 세 번째 파라미터로 전달함.
람다를 사용하면 임의 클래스를 사용하는 것보다 간결함.
``java
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ?",
(ResultSet rs, int rowNum) -> {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
},
email
);
``
동일한 RowMapper 구현을 여러 곳에서 사용한다면 RowMapper 인터페이스를 구현한 클래스를 만들어서 코드 중복을 막을 수 있음.
``java
public class MemberRowMapper implements RowMapper<Member> {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
}
List<Member> results = jdbcTemplate.query(
"select * from MEMBER where EMAIL = ? and NAME = ?",
new MemberRowMapper(),
email, name
);
``
selectByEmail() 메서드는 지정한 이메일에 해당하는 MEMBER 데이터가 존재하면 해당 Member 객체를 리턴하고, 그렇지 않으면 null을 리턴함.
## 4.3 결과가 1행인 경우 사용할 수 있는 queryForObject() 메서드
다음은 MEMBER 테이블의 전체 행 개수를 구하는 코드임.
query() 메서드를 사용했음.
``java
public int count() {
List<Integer> results = jdbcTemplate.query(
"select count(*) from MEMBER",
new RowMapper<Integer>() {
@Override
public Integer mapRow(ResultSet rs, int rowNum) throws SQLException {
return rs.getInt(1);
}
}
);
return results.get(0);
}
``
count(*) 쿼리는 결과가 한 행 뿐이므로 쿼리 결과를 List로 받기보다는 Integer와 같은 정수 타입으로 받으면 더 편리할 것임.
이를 위한 메서드가 queryForObject()임.
``java
public int count() {
Integer count = jdbcTemplate.queryForObject(
"select count(*) from MEMBER",
Integer.class
);
return count;
}
``
이렇게 하면 count(*) 쿼리 실행 코드를 더 간단하게 구현할 수 있음.
## 4.3 결과가 1행인 경우 사용할 수 있는 queryForObject() 메서드
queryForObject() 메서드는 쿼리 실행 결과 행이 한 개인 경우에 사용할 수 있는 메서드이다.
queryForObject() 메서드의 두 번째 파라미터는 칼럼을 읽어올 때 사용할 타입을 지정한다.
예를 들어 평균을 구할 때는 다음과 같이 Double 타입을 사용할 수 있다.
``java
double avg = jdbcTemplate.queryForObject(
"select avg(height) from FURNITURE where TYPE=? and STATUS=?",
Double.class,
100, "S"
);
``
이 코드에서 볼 수 있듯이 queryForObject() 메서드도 쿼리에 인덱스 파라미터(물음표)를 사용할 수 있음.
인덱스 파라미터가 존재하면 파라미터의 값을 가변 인자로 전달함.
실행 결과 칼럼이 두 개 이상이면 RowMapper를 파라미터로 전달해서 결과를 생성할 수 있다.
예를 들어 특정 ID를 갖는 회원 데이터를 queryForObject()로 읽어오고 싶다면 다음 코드를 사용할 수 있음.
``java
Member member = jdbcTemplate.queryForObject(
"select * from MEMBER where ID = ?",
new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(
rs.getString("EMAIL"),
rs.getString("PASSWORD"),
rs.getString("NAME"),
rs.getTimestamp("REGDATE").toLocalDateTime());
member.setId(rs.getLong("ID"));
return member;
}
},
100
);
``
queryForObject() 메서드를 사용한 위 코드와 기존의 query() 메서드를 사용한 코드의 차이점은 리턴 타입이 List가 아니라
RowMapper로 변환해주는 타입(위 코드에서는 Member)이라는 점이다.
주요 queryForObject() 메서드는 다음과 같다.
T queryForObject(String sql, Class<T> requiredType)
T queryForObject(String sql, Class<T> requiredType, Object... args)
T queryForObject(String sql, RowMapper<T> rowMapper)
T queryForObject(String sql, RowMapper<T> rowMapper, Object... args)
queryForObject() 메서드를 사용하려면 쿼리 실행 결과는 반드시 한 행이어야 한다. 만약 쿼리 실행 결과 행이 없거나 두 개 이상이면 IncorrectResultSizeDataAccessException이 발생한다. 행의 개수가 0이면 하위 클래스인 EmptyResultDataAccessException이 발생한다. 따라서 결과 행이 정확히 한 개가 아니면 queryForObject() 메서드 대신 query() 메서드를 사용해야 한다.
## 4.4 JdbcTemplate을 이용한 변경 쿼리 실행
INSERT, UPDATE, DELETE 쿼리는 update() 메서드를 사용한다.
int update(String sql)
int update(String sql, Object... args)
update() 메서드는 쿼리 실행 결과로 변경된 행의 개수를 리턴한다. update() 메서드의 사용 예는 다음과 같다.
``java
package spring;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void update(Member member) {
jdbcTemplate.update(
"update MEMBER set NAME = ?, PASSWORD = ? where EMAIL = ?",
member.getName(), member.getPassword(), member.getEmail()
);
}
}
``
## 4.5 PreparedStatementCreator를 이용한 쿼리 실행
쿼리에서 사용할 값을 인자로 전달하는 방법 외에도 PreparedStatementCreator를 인자로 받아 직접 PreparedStatement를 생성하고 설정해야 할 때가 있다.
PreparedStatementCreator 인터페이스는 아래와 같다.
``java
package org.springframework.jdbc.core;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public interface PreparedStatementCreator {
PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}
``
PreparedStatementCreator 인터페이스의 createPreparedStatement() 메서드는 Connection 타입의 파라미터를 가진다.
PreparedStatementCreator를 구현한 클래스는 createPreparedStatement() 메서드의 파라미터로 전달받는 Connection을 이용해서
PreparedStatement 객체를 생성하고 인덱스 파라미터를 알맞게 설정한 뒤에 리턴하면 된다.
PreparedStatementCreator 인터페이스 예제 코드임
``java
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
// 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
PreparedStatement pstmt = con.prepareStatement(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) values (?, ?, ?, ?)"
);
// 인덱스 파라미터의 값 설정
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
// 생성한 PreparedStatement 객체 리턴
return pstmt;
}
});
``
JdbcTemplate 클래스가 제공하는 메서드 중에서 PreparedStatementCreator 인터페이스를 파라미터로 갖는 메서드는 다음과 같다.
List<T> query(PreparedStatementCreator psc, RowMapper<T> rowMapper)
int update(PreparedStatementCreator psc)
int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
위 목록에서 세 번째 메서드는 자동 생성되는 키값을 구할 때 사용한다.
## 4.6 INSERT 쿼리 실행 시 KeyHolder를 이용해서 자동 생성 키값 구하기
MySQL AUTO_INCREMENT 칼럼은 행이 추가되면 자동으로 값이 할당되는 칼럼으로서 주로 기본 키 칼럼에 사용된다.
예를 들어 MEMBER 테이블을 생성할 때 사용한 쿼리도 다음 코드처럼 기본 키 칼럼을 AUTO_INCREMENT 칼럼으로 지정했다.
``sql
create table spring5fs.MEMBER (
ID int auto_increment primary key,
EMAIL varchar(255),
PASSWORD varchar(100),
NAME varchar(100),
REGDATE datetime,
unique key (EMAIL)
) engine=InnoDB character set=utf8;
``
AUTO_INCREMENT와 같은 자동 증가 칼럼을 가진 테이블에 값을 삽입하면 해당 칼럼의 값이 자동으로 생성된다.
따라서 아래 코드처럼 INSERT 쿼리에 자동 증가 칼럼에 해당하는 값은 지정하지 않음.
``java
jdbcTemplate.update(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) values (?, ?, ?, ?)",
member.getEmail(), member.getPassword(), member.getName(),
new Timestamp(member.getRegisterDate().getTime())
);
``
쿼리 실행 후에 생성된 키값을 알고 싶다면 어떻게 해야 할까?
update() 메서드는 변경된 행의 개수를 리턴할 뿐 생성된 키값을 리턴하지는 않는다.
JdbcTemplate은 자동으로 생성된 키값을 구할 수 있는 방법을 제공하고 있는데,
그것은 바로 KeyHolder를 사용하는 것이다.
KeyHolder를 사용하면 다음과 같이 MemberDao의 insert() 메서드에서 삽입하는 Member 객체의 ID 값을 구할 수 있다.
``java
package spring;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
public class MemberDao {
private JdbcTemplate jdbcTemplate;
public MemberDao(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void insert(final Member member) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement pstmt = con.prepareStatement(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
"values (?, ?, ?, ?)",
new String[]{"ID"}
);
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
return pstmt;
}
}, keyHolder);
Number keyValue = keyHolder.getKey();
member.setId(keyValue.longValue());
}
}
``
GeneratedKeyHolder 객체를 생성한다.
이 클래스는 자동 생성된 키값을 구해주는 KeyHolder 구현 클래스이다.
update() 메서드는 PreparedStatementCreator 객체와 KeyHolder 객체를 파라미터로 갖는다.
PreparedStatementCreator 임의 클래스를 이용해서 PreparedStatement 객체를 직접 생성한다.
여기서 주목할 점은 Connection의 prepareStatement() 메서드를 이용해서
PreparedStatement 객체를 생성하는데 두 번째 파라미터로 String 배열인 {"ID"}를 준다.
이 두 번째 파라미터는 자동 생성되는 키 칼럼 목록을 지정할 때 사용한다.
JdbcTemplate.update() 메서드의 두 번째 파라미터로 KeyHolder 객체를 전달한다.
아래는 람다식을 사용한 예제 이다.
``java
jdbcTemplate.update((Connection con) -> {
PreparedStatement pstmt = con.prepareStatement(
"insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
"values (?, ?, ?, ?)",
new String[]{"ID"}
);
pstmt.setString(1, member.getEmail());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getName());
pstmt.setTimestamp(4, new Timestamp(member.getRegisterDate().getTime()));
return pstmt;
}, keyHolder);
``
자바 8을 더욱 코드를 간략화 할 수 있다.
이 책에서는 자바8 사용을 권장하는듯 하다.
## 5. MemberDao 테스트
위 코드를 보면 JdbcTemplate을 이용해서 MemberDao 클래스를 완성했다.
메인 클래스를 작성해서 MemberDao가 정상적으로 동작하는지 확인해보자.
``java
package config;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.MemberDao;
@Configuration
public class AppCtx {
@Bean(destroyMethod = "close")
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
ds.setUsername("spring5");
ds.setPassword("spring5");
ds.setInitialSize(2);
ds.setMaxActive(10);
ds.setTestWhileIdle(true);
ds.setMinEvictableIdleTimeMillis(60000 * 3);
ds.setTimeBetweenEvictionRunsMillis(10000);
return ds;
}
@Bean
public MemberDao memberDao() {
return new MemberDao(dataSource());
}
}
``
``java
package main;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.AppCtx;
import spring.Member;
import spring.MemberDao;
public class MainForMemberDao {
private static MemberDao memberDao;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
memberDao = ctx.getBean(MemberDao.class);
selectAll();
updateMember();
insertMember();
ctx.close();
}
private static void selectAll() {
System.out.println("----- selectAll");
int total = memberDao.count();
System.out.println("Total: " + total);
List<Member> members = memberDao.selectAll();
for (Member m : members) {
System.out.println(m.getId() + ": " + m.getEmail() + ": " + m.getName());
}
}
private static void updateMember() {
System.out.println("----- updateMember");
Member member = memberDao.selectByEmail("[email protected]");
if (member != null) {
String oldPw = member.getPassword();
String newPw = Double.toHexString(Math.random());
member.changePassword(oldPw, newPw);
memberDao.update(member);
System.out.println("Password changed from " + oldPw + " to " + newPw);
}
}
private static void insertMember() {
System.out.println("----- insertMember");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMddHHmmss");
String prefix = formatter.format(LocalDateTime.now());
Member member = new Member(prefix + "@test.com", prefix, prefix, LocalDateTime.now());
memberDao.insert(member);
System.out.println(member.getId() + " : " + member.getEmail());
}
}
``
selectAll() 메서드는 memberDao.count() 메서드를 실행해서 전체 행의 개수를 구하고,
memberDao.selectAll() 메서드를 이용해서 전체 Member 데이터를 구한 뒤 콘솔에 출력함.
updateMember() 메서드는 EMAIL 칼럼 값이 "[email protected]"인 Member 객체를 구한 뒤 임의의 새로운 암호로 변경함.
insertMember() 메서드는 현재 시간을 기반으로 새로운 Member를 생성하고 삽입함. |
5. MemberDao 테스트MainForMemberDao 클래스 실행
이 에러 메시지는 MySQL 서버에 연결할 권한이 없는 경우 발생함. 잘못된 DB 연결 정보 예시
로컬에 설치된 DBMS를 이용해서 테스트할 때 이런 에러가 발생하는 이유는 주로 DBMS를 실행하지 않았기 때문임. 잘못된 쿼리 예시
잘못된 부분이 없는 것 같지만,
``plaintext DB 연동 과정에서 자주 발생하는 세 가지 종류의 예외를 살펴봤음. 이 외에도 에러 메시지를 보면 문제 발생 원인을 찾는 데 도움이 됨. DB 연동 코드를 실행하는 과정에서 예외가 발생하면 당황하지 말고 예외 메시지를 차분히 살펴보는 습관을 들이자. 6. 스프링의 예외 변환 처리SQL 문법이 잘못됐을 때 발생한 메시지를 보면 예외 클래스가 org.springframework.jdbc 패키지에 속한 BadSqlGrammarException 클래스임을 알 수 있음. 에러 메시지를 보면 BadSqlGrammarException이 발생한 이유는 MySQLSyntaxErrorException이 발생했기 때문임.
위와 같은 예외가 발생할 때 사용한 코드는 다음과 같음: ``java BadSqlGrammarException을 발생한 메서드는 JdbcTemplate 클래스의 update() 메서드임. JdbcTemplate의 update() 메서드는 DB 연동을 위해 JDBC API를 사용하는데, JDBC API를 사용하는 과정에서 SQLException이 발생하면 이 예외를 알맞은 DataAccessException으로 변환해서 발생함. 따라서 다음과 유사한 방식으로 예외를 변환해서 재발생함:
예를 들어사, DataAccessException은 스프링이 제공하는 예외 타입으로 데이터 연결에 문제가 있을 때 스프링 모듈이 발생시킴. ``java try { try { // 스프링 스프링의 연동 기능을 사용하면 예외를 동일한 방식으로 처리할 수 있음. BadSqlGrammarException은 DataAccessException을 상속받은 하위 타입임. BadSqlGrammarException은 실행할 쿼리가 올바르지 않은 경우에 사용됨. 스프링은 이 외에도 DuplicateKeyException, QueryTimeoutException 등 DataAccessException을 상속한 다양한 예외 클래스를 제공함. 각 예외 클래스의 이름은 문제가 발생한 원인을 의미함. 따라서 예외가 발생한 경우 예외 타입의 이름만으로도 어느 정도 문제 원인을 유추할 수 있음. DataAccessException은 RuntimeException임. JDBC를 직접 이용하면 다음과 같이 try~catch를 이용해서 예외를 처리해야한다.
jdbcTemplate.update(someQuery, param1); 7. 트랜잭션 처리이메일이 유효한지 여부를 판단하기 위해 실제로 검증 목적의 메일을 발송하는 서비스를 사용한 경험이 있을 것임. 이들 서비스는 이메일에 함께 보낸 링크를 클릭하면 최종적으로 이메일이 유효하다고 판단하고 해당 이메일을 사용할 수 있도록 함. 이렇게 이메일 인증 시점에 테이블의 데이터를 변경하는 기능은 다음 코드처럼 회원 정보에서 이메일을 수정하고 인증 상태를 변경하는 두 쿼리를 실행할 것임. ``java 예를 들어, 코드를 잘못 수정/배포해서 두 번째 쿼리에서 사용할 테이블 이름이 잘못되었을 수도 있고, 중복된 값이 존재해서 INSERT 쿼리를 실행하는데 실패할 수도 있음. 두 번째 쿼리가 실패했음에도 불구하고 첫 번째 쿼리 실행 결과가 DB에 반영되면 이후 해당 사용자의 이메일 주소는 인증되지 않은 채로 계속 남아 있게 됨. 따라서 두 번째 쿼리 실행에 실패하면 첫 번째 쿼리 실행 결과도 취소해야 올바른 상태를 유지할 수 있음. 이렇게 두 개 이상의 쿼리를 한 작업으로 실행해야 할 때 사용하는 것이 트랜잭션임. 트랜잭션은 여러 쿼리를 논리적으로 하나의 작업으로 묶어줌. 한 트랜잭션으로 묶인 쿼리 중 하나라도 실패하면 전체 쿼리를 실패로 간주하고 실패 이전에 실행한 쿼리를 취소함 반면에 트랜잭션으로 묶인 모든 쿼리가 성공해서 쿼리 결과를 DB에 실제로 반영하는 것을 커밋(commit)이라고 함. 트랜잭션을 시작하면 트랜잭션을 커밋하거나 롤백할 때까지 실행한 쿼리들이 하나의 작업 단위가 됨. ``java
} catch (SQLException ex) { 7.1 @transactional을 이용한 트랜잭션 처리스프링이 제공하는 @transactional 애노테이션을 사용하면 트랜잭션 범위를 매우 쉽게 지정할 수 있음. 다음과 같이 트랜잭션 범위에서 실행하고 싶은 메서드에 @transactional 애노테이션만 붙이면 됨. ``java public class ChangePasswordService {
} @transactional 애노테이션이 제대로 동작하려면 다음의 두 가지 내용을 스프링 설정에 추가해야 함: 플랫폼 트랜잭션 매니저(PlatformTransactionManager) 빈 설정=> @transactional 애노테이션 활성화 설정 ``java @configuration
} PlatformTransactionManager는 스프링이 제공하는 트랜잭션 매니저 인터페이스임. 스프링은 구현기술에 상관없이 동일한 방식으로 트랜잭션을 처리하기 위해 이 인터페이스를 사용함. JDBC는 DataSourceTransactionManager 클래스를 PlatformTransactionManager로 사용함. @EnableTransactionManagement 애노테이션은 @transactional 애노테이션이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능을 활성화함. 등록된 PlatformTransactionManager 빈을 사용해서 트랜잭션을 적용함. 트랜잭션 처리를 위한 설정을 완료하면 트랜잭션 범위에서 실행하고 싶은 스프링 빈 객체의 메서드에 @transactional 애노테이션을 붙이면 됨. ``java import org.springframework.transaction.annotation.Transactional; public class ChangePasswordService {
} 7.1 트랜잭션 테스트AppCtx 설정 ``java import org.apache.tomcat.jdbc.pool.DataSource; import spring.ChangePasswordService; @configuration
} MainForCPS 클래스 ``java import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class MainForCPS { Logback 설정트랜잭션 관련 로그 메시지를 출력하기 위해 Logback 설정을 추가한다. pom.xml 파일에 Logback 의존성을 추가한다.
src/main/resources 폴더에 logback.xml 파일을 작성. ``xml %d %5p %c{2} - %m%n ``실행 결과 확인MainForCPS 클래스를 실행하면 트랜잭션이 시작되고 커밋되며, 로그 메시지가 콘솔에 출력된다.
로그를 통해 트랜잭션이 시작되고 커밋되는 과정을 확인할 수 있다. 예외 처리와 트랜잭션 롤백WrongIdPasswordException이 발생하면 트랜잭션이 롤백된다. 로그 메시지를 통해 이를 확인할 수 있다.
트랜잭션을 롤백한다는 로그 메시지가 출력된다. @transactional과 프록시 트랜잭션 롤백 처리
이 코드의 실행 결과를 보면 WrongIdPasswordException이 발생했을 때 트랜잭션이 롤백된다.
@transactional의 rollbackFor 속성을 설정하면 @transactional의 주요 속성 1. JDBC 프로그래밍의 단점을 보완하는 스프링
JDBC API를 이용한 DB 연동 코드 구조는 반복되는 코드가 많다.
이 코드에서 스프링의 장점은 트랜잭션 관리가 쉽다는 것이다. @transactional 애노테이션을 사용하면 자동으로 트랜잭션 처리를 할 수 있다.
2. 프로젝트 준비2.1 프로젝트 생성 프로젝트를 위한 sp5-chap08 폴더를 생성. sp5-chap08의 하위 폴더로 src\main\java 폴더를 생성. sp5-chap08 폴더에 pom.xml 파일을 생성. 2.2 DB 테이블 생성MySQL에 root 사용자로 연결한 뒤 다음 쿼리를 실행해서 DB 사용자, 데이터베이스, 테이블을 생성한다.
이 쿼리를 실행하면 spring5 계정으로 접속할 수 있고, MEMBER 테이블이 생성된다. 3. DataSource 설정JDBC API는 DriverManager 외에 DataSource를 이용해서 DB 연결을 구하는 방법을 제공한다.
3.1 Tomcat JDBC의 주요 프로퍼티Tomcat JDBC 모듈의 DataSource 클래스는 커넥션 풀 기능을 제공한다.
4. JdbcTemplate을 이용한 쿼리 실행4.1 JdbcTemplate 생성하기``java
} 4.2 JdbcTemplate을 이용한 조회 쿼리 실행
4.3 queryForObject() 메서드
7. 트랜잭션 처리7.1 @transactional을 이용한 트랜잭션 처리
7.2 @transactional과 프록시스프링은 @transactional 애노테이션을 적용하기 위해 내부적으로 AOP를 사용한다. ``java
} 스프링은 프록시 객체를 생성하여 @transactional 애노테이션이 적용된 메서드를 트랜잭션 범위에서 실행한다.
7.3 트랜잭션 롤백 처리@transactional 애노테이션을 사용하면 RuntimeException이 발생할 때 자동으로 트랜잭션을 롤백한다.
7.4 @transactional의 주요 속성value String 트랜잭션을 관리할 때 사용할 PlatformTransactionManager 빈의 이름 propagation Propagation 트랜잭션 전파 타입을 지정 isolation Isolation 트랜잭션 격리 레벨을 지정 timeout int 트랜잭션 제한 시간을 지정 (초 단위) 7.5 @EnableTransactionManagement 애노테이션의 주요 속성proxyTargetClass 클래스를 이용해서 프록시를 생성할지 여부를 지정 order AOP 적용 순서를 지정 7.6 트랜잭션 전파트랜잭션 전파 유형은 다음과 같다:
8. 전체 기능 연동한 코드 실행완성된 AppCtx 설정 클래스와 콘솔을 이용한 Main 클래스 예제는 다음과 같다. ``java import org.apache.tomcat.jdbc.pool.DataSource; @configuration
} ``java import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.io.BufferedReader; public class Main {
} 이와 같이 spring과 DB의 연결 방법과 상호작용법을 알아 보았습니다. |
스프링 프로그래밍 입문 5 남은 부분 정독
The text was updated successfully, but these errors were encountered: