@Pointcut("execution(public * ex04..*(..))")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
포인트컷 지정자
* : 모든 값
.. : 0개 이상
aop/CacheAspect
package com.test.test1.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import java.util.HashMap;
import java.util.Map;
@Aspect
public class CacheAspect {
// factorial(10) => 3628800
// { 10: 3628800 }
private Map<Long, Object> cache = new HashMap();
@Pointcut("execution(public * ex04..*(..)")
public void cacheTarget() {
}
// factorial(10) => 3628800
@Around("cacheTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long num = (Long)joinPoint.getArgs()[0];
if (cache.containsKey(num)) {
System.out.printf("CacheAspect: cache에서 가져옴 [%d]\n", num);
return cache.get(num);
}
Object result = joinPoint.proceed();
cache.put(num, result);
System.out.printf("CacheAspect: cache에 추가 [$d]\n", num);
return result;
}
}
AppCtxAspect
@Bean
public CacheAspect cacheAspect() {
return new CacheAspect();
}
MainForAspect
public class MainForAspect {
public static void main(String[] args) {
AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtxAspect.class);
Calculator rec = ctx.getBean("recCalculator", Calculator.class);
System.out.println(rec.factorial(10));
System.out.println(rec.factorial(10));
System.out.println(rec.factorial(7));
System.out.println(rec.factorial(7));
System.out.println(rec.factorial(5));
ctx.close();
}
}
실행 결과
Advice의 적용 순서는 설정 클래스에 빈 순서와 동일하게 동작 => 스프링 프레임워크와 자바 버전에 따라 상이
=>@Order
사용해서 명시적으로 순서를 지정
@Aspect
@Order(1)
public class ExeTimeAspect {
@Aspect
@Order(2)
public class CacheAspect {
실행 결과
@Pointcut("execution(public * ex04..*(..))")
private void publicTarget() { }
@Around("publicTarget()")
@Around 어노테이션에 execution 명시자를 직접 지정하는 것도 가능하다.
@Around("execution(public * ex04..*(..))")
but, 위에 걸 쓰는 이유는? => Pointcut을 재사용하기 위해 쓴다.
@Aspect
@Order(1)
public class ExeTimeAspect {
@Pointcut("execution(public * ex04..*(..))")
public void publicTarget() { // 다른 클래스에서 사용할 수 있도록 public
}
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
@Aspect
@Order(2)
public class CacheAspect {
// factorial(10) => 3628800
// { 10: 3628800 }
private Map<Long, Object> cache = new HashMap<>();
/*
@Pointcut("execution(public * ex04..*(..))")
public void cacheTarget() {
}
*/
@Around("aspect.ExeTimeAspect.publicTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
CommonPointcut
package com.test.test1.aop;
import org.aspectj.lang.annotation.Pointcut;
public class CommonPointcut {
@Pointcut("execution(public * com.test.test1.ex04..*(..))")
public void commonTarget() {
}
}
ExeTimeAspect
package com.test.test1.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import java.util.Arrays;
@Aspect
@Order(1)
public class ExeTimeAspect {
/*
@Pointcut("execution(public * com.test.test1.ex04...*(..))")
private void publicTarget() {
}
*/
@Around("aop.CommonPointcut.commonTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try{
Object result = joinPoint.proceed();
return result;
} finally {
long end = System.nanoTime();
System.out.printf("%s.%s(%s) 실행결과 = %d \n",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
Arrays.toString(joinPoint.getArgs()),
(end - start)
);
}
}
}
@Around("aop.CommonPointcut.commonTarget()")
CacheAspect
package com.test.test1.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import java.util.HashMap;
import java.util.Map;
@Aspect
@Order(2)
public class CacheAspect {
// factorial(10) => 3628800
// { 10: 3628800 }
private Map<Long, Object> cache = new HashMap<>();
/*
@Pointcut("execution(public * com.test.test1.ex04..*(..)")
public void cacheTarget() {
}
*/
// factorial(10) => 3628800
@Around("aspect.CommonPointcut.commonTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long num = (Long)joinPoint.getArgs()[0];
if (cache.containsKey(num)) {
System.out.printf("CacheAspect: cache에서 가져옴 [%d]\n", num);
return cache.get(num);
}
Object result = joinPoint.proceed();
cache.put(num, result);
System.out.printf("CacheAspect: cache에 추가 [$d]\n", num);
return result;
}
}
@Around("aspect.CommonPointcut.commonTarget()")
스프링 기반 애플리케이션을 빠르고 쉽게 개발
http://localhost:8080/ <= / (Web Document Root) 디렉토리 웹 서버에서 정의한 기본 페이지를 검색
404 Not Found 인 이유: 이 서버에는 아무것도 없기 때문에 !!
package com.test.sample.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
// @RequestMapping("/")
// @RequestMapping(value = "/", method = RequestMethod.GET)
@GetMapping("/")
public String hello() {
return "Hello World!!!";
}
}
@RestController
사용자 요청에 대한 응답으로 데이터를 내려보내주는 것
@GetMapping("/") = @RequestMapping("/")
= @RequestMapping(value = "/", method = RequestMethod.GET)
- "/"로 요청이 들어오면 반환값을 응답해준다.
GET URI HTTP/1.1 <= 요청 시작 -> 방식(method) URL 프로토콜
요청헤더: 헤더값 <= 헤더 ex) Authorization
요청헤더: 헤더값
요청헤더: 헤더값
<= 헤더 끝 (아무 내용 없이 한 줄 띄움)
HTTP/1.1 200 <= 응답시작
응답헤더: 헤더값 <= 헤더
name=value&name=value <= 요청 본문 (서버로 전달하는 값으로 방식에 따라 있을 수도 있고 있음)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.10.1
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 14
< Date: Wed, 22 Jan 2025 01:49:32 GMT
<
http://www.test.com:8080/path/subpath/file?name=value&name=value#abcd
~~~~ ~~~~~~~~~~~~ ~~~~
스킴 호스트주소 포트
프로토콜 도메인/IP
@SpringBootApplication
package com.test.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
독립 실행 => 내장 웹 서버(WAS. ex)tomcat)를 포함
DevTools => 코드 변경 시 자동 재시작, 라이브 리로딩, 개발 모드 전용 설정 등 사용이 가능
프로덕션 준비 기능 => actuator => 모니터 및 관리 기능을 제공
간편한 설정
CLI
애플리케이션의 데이터를 나타내며, 데이터의 구조와 비즈니스 로직을 정의하는 데 사용
도메인 객체, 폼 객체, 데이터 전송 객체, 리포지토리 등으로 구성
도메인 객체(Domain Object)
폼 객체(Form Object)
데이터 전송 객체(DTO, Data Transfer Object)
리포지토리(Repository)
사용자 요청을 처리하고 적절한 응답을 생성하는 역할
사용자가 요청한 URL을 매핑하여 해당 요청을 처리하는 메서드를 호출하고, 모델 데이터를 생성하거나 수정한 후에 적절한 뷰를 선택하여 응답을 반환
요청 매핑 (Request Mapping)
모델 처리 (Model Handling)
뷰 선택 (View Selection)
폼 데이터 처리 (Form Handling)
Dispatcher Servlet에서 핸들러를 조회하고 매핑한다.(1) -> 핸들러가 url 주소를 봐서 해당 컨트롤러를 조회한다. (2)
-> (3) -> (4) -> 컨트롤러 동작 -> 데이터 생성, 조작 -> (5) -> 어떤 View를 써야할까? -> (6) -> (7) -> (8)
애플리케이션의 다른 부분과 저장소 간의 상호작용을 담당하는 계층
비즈니스 로직과 데이터 접근 코드를 분리해 애플리케이션의 유지 보수성과 확장성을 높이는 것이 목적
- DAO : 상호작용. CRUD 기능 구현. 앞단 서비스로 인해 이용되는 것. (연결)
- JDBC Template : DB와 연동하려면 인터페이스(명세. Interface)를 정의
- JDBC Driver : 구현체 = Driver - MySQL JDBC Driver가 있어야 MySQL과 연동할 수 있다.
- DataSource : JDBC 명세의 일부분. 서버와의 연결
MySQL workbench에서 테이블 생성 ⬇️
create table t_board (
board_idx int(11) not null auto_increment comment "글 번호",
title varchar(300) not null comment "제목",
contents text not null comment "내용",
hit_cnt smallint(10) not null default "0" comment "조회수",
created_dt datetime not null comment "작성일시",
created_id varchar(300) not null comment "작성자",
updator_dt datetime null comment "수정일시",
updator_id varchar(300) null comment "수정자",
deleted_yn char(1) not null default "N" comment "삭제여부",
primary key (board_idx)
);
데이터베이스와 상호작용을 담당하는 객체
개발자는 SQL을 사용해 CRUD 기능을 직접 구현해야 한다.
애플리케이션의 Object (SQL을 이용해 CRUD) DB의 Record 단위의 처리
~~~~~~ ^ ~~~~~~
| | |
+----------------|-------------------------+
|
불일치를 해결해야 함
개발자가 SQL을 이용해 불일치를 해결할 수 있다.
JPA: 불일치 자동으로 해결해주는 것
DB에 접근할 수 있도록 Java에서 제공하는 API
데이터베이스에서 자료를 추가, 검색, 수정, 삭제하는 방법을 제공
=> JPA , QueryDSL 많이 쓴다.
Plain JDBC API의 문제점을 해결하기 위해 스프링에서 제공하는 Spring JDBC 접근 방법 중 하나
~~~~~~~~~~~~~~~~~~~~~
DataSource 설정 → Connection 생성 → SQL 작성 → Statement 생성 → Statement 실행 → ResultSet 처리 → 결과 출력 → Exception 처리 → Transaction 처리 → ResultSet 닫기 → Statement 닫고 → Connection 닫고
여기서 SQL 작성과 결과 출력(결과 사용)은 개발자가 해줘야하는 것 !!
나머지는 반복되는 것이고 자동화할 수 있다. => JDBC Template이 수행
=> 반복되는 많은 단순 작업들이 빠질 수 있다 !!
application.properties
spring.application.name=board
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/springbootdb?useUnicode=true&characterEncoding=utf-8&serverTimeZone=Asia/Seoul
spring.datasource.hikari.username=root
spring.datasource.hikari.password=[MySQL password]
spring.datasource.hikari.connection-test-query=select 1
Connection URL Syntax
Configuration Properties
DatabaseConfiguration
package board.configuration;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import javax.sql.DataSource;
@Configuration
@PropertySource("classpath:/application.properties")
public class DatabaseConfiguration {
@Bean
@ConfigurationProperties(prefix="spring.datasource.hikari")
HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
DataSource dataSource() {
DataSource dataSource = new HikariDataSource();
System.out.println(dataSource);
return dataSource;
}
}
💡 MyBatis 란 ?
- 자바 애플리케이션에서 관계형 데이터베이스와 상호작용하기 위한 퍼시스턴스 프레임워크
- 객체와 SQL 구문의 자동 맵핑을 제공해 개발 생산성을 향상
DatabaseConfiguration
package board.configuration;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
@PropertySource("classpath:/application.properties")
public class DatabaseConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
DataSource dataSource() {
DataSource dataSource = new HikariDataSource(hikariConfig());
System.out.println(dataSource);
return dataSource;
}
// SqlSessionFactory: SqlSession 객체를 생성하는 역할을 담당하는 인터페이스
// 데이터베이스 연결, 트랜잭션 관리, 매퍼 파일의 위치 등 MyBatis 설정 정보를 포함
// SqlSession: MyBatis에서 데이터베이스와 상호작용(SQL 실행, 트랜잭션을 관리, ...)하는 인터페이스
@Bean
SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setMapperLocations(
applicationContext.getResources("classpath:/mapper/**/sql-*.xml")
);
return sessionFactoryBean.getObject();
}
// SqlSessionTemplate: MyBatis와 스프링 프레임워크를 통합할 때 사용하는 클래스
// SqlSesstion 인터페이스를 구현
@Bean
SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
SqlSessionFactory
SqlSession
SqlSessionTemplate
📎 DB와 상호작용할 수 있는 인터페이스 반환 방법
1. 연결 정보가 담겨있는 dataSource를 던져주기. DB 연결
2. sql로 시작하는 xml 파일이 mapper 파일이라는 것을 알려준다.
🚨 class path resource [mapper/] cannot be resolved to URL because it does not exist
=> resources 밑에 mapper 폴더만 만들어주면 해결 된다.
BoardDto
package board.dto;
import lombok.Data;
@Data
public class BoardDto {
private int boardIdx;
private String title;
private String contents;
private int hitCnt;
private String createdDt;
private String createdId;
private String updatorDt;
private String updatorId;
}
java의 boardIdx - MyBatis - db의 board_idx
MyBatis가 중간 역할 잘 하도록 설정하자 !
application.properties
mybatis.configuration.map-underscore-to-camel-case=true
DatabaseConfiguration
@Bean
@ConfigurationProperties(prefix = "mybatis.configuration")
org.apache.ibatis.session.Configuration mybatisConfig() {
return new org.apache.ibatis.session.Configuration();
}
BoardController
서비스가 가지고 있는 메서드를 호출해야 한다.
그리고 서비스가 반환해주는 값을 반환해줄 것이다.
=> 서비스를 의존한다.
package board.controller;
import board.dto.BoardDto;
import board.service.BoardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@Controller
public class BoardController {
@Autowired
private BoardService boardService;
@GetMapping("/board/openBoardList.do")
public ModelAndView openBoardList() throws Exception {
ModelAndView mv = new ModelAndView("/board/boardList");
List<BoardDto> list = boardService.selectBoardList();
mv.addObject("list", list);
return mv;
}
}
BoardService
package board.service;
import board.dto.BoardDto;
import java.util.List;
public interface BoardService {
List<BoardDto> selectBoardList();
}
BoardServiceImpl
package board.service;
import board.dto.BoardDto;
import board.mapper.BoardMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardMapper boardMapper;
@Override
public List<BoardDto> selectBoardList() {
return boardMapper.selectBoardList();
}
}
spring 코드랑 sql이랑 연결해주는 역할
BoardMapper
package board.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import board.dto.BoardDto;
@Mapper
public interface BoardMapper {
List<BoardDto> selectBoardList();
}
sql-board.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="board.mapper.BoardMapper">
<select id="selectBoardList" resultType="board.dto.BoardDto">
select board_idx, title, hit_cnt, date_format(created_dt, '%Y.%m.%d %H:%i:%s') as created_dt from t_board
where deleted_yn = 'N'
order by board_idx desc
</select>
</mapper>
@Mapper로 지정하고 namespace와 id를 연결하면 MyBatis가 구현클래스를 자동으로 만들어준다 !! 😆
boardList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>게시판</title>
</html>
<body>
<div class="container">
<h2>게시판 목록</h2>
<table class="board_list">
<colgroup>
<col width="15%" />
<col width="*" />
<col width="15%" />
<col width="20%" />
</colgroup>
<thead>
<tr>
<th scope="col">글번호</th>
<th scope="col">제목</th>
<th scope="col">조회수</th>
<th scope="col">작성일</th>
</tr>
</thead>
<tbody>
<tr th:if="${#lists.size(list)} > 0" th:each="list : ${list}">
<td th:text="${list.boardIdx}">00</td>
<td th:text="${list.title}" class="title">게시판 글 제목</td>
<td th:text="${list.hitCnt}">00</td>
<td th:text="${list.createdDt}">2025.01.22 15:49:00</td>
</tr>
<tr th:unless="${#lists.size(list)} > 0">
<td colspang="4">조회된 결과가 없습니다.</td>
</tr>
</tbody>
</table>
</div>
</body>
http://localhost:8080/board/openBoardList.do
insert into t_board (title, contents, created_datetime, creator_id) values ('첫번째 게시글', '첫번째 게시글 입니다.', now(), 'tester');
insert into t_board (title, contents, created_datetime, creator_id) values ('두번째 게시글', '두번째 게시글 입니다.', now(), 'tester');
insert into t_board (title, contents, created_datetime, creator_id) values ('세번째 게시글', '세번째 게시글 입니다.', now(), 'tester');
서버 사이드 자바 템플릿 엔진으로 스프링 MVC와 사용
HTML, XML, JS, CSS 등의 템플릿을 처리할 수 있으며, 내추럴 템플릿 방식으로 동작
https://www.thymeleaf.org/doc/articles/standarddialect5minutes.html
${...} : Variable expressions / 변수 표현식 / 모델 객체의 값을 참조
*{...} : Selection expressions / 선택 변수 표현식 / 선택된 객체의 속성을 참조
#{...} : Message (i18n) expressions / 메시지 표현식 / 메시지 번들을 참조
@{...} : Link (URL) expressions / 링크 (URL) 표현식 / 링크(URL)을 생성
~{...} : Fragment expressions / 조각 표현식 / 템플릿 조각을 참조
th:text 텍스트 컨텐츠를 설정 / <p th:text="${dto.title}"></p>
th:href 링크 URL을 설정 / <a th:href="@{/users/{id}(id=${user.id})}">user detail</a>
⇒ user 객체의 id 속성의 값이 123이라면
<a href="/users/123">user detail</a>
th:each 컬렉션을 반복 / <tr th:each="user : ${users}"> ... </tr>
th:if / th:unless 조건부 렌더링 / <p th:if="${users.active}">active</p>
<p th:unless="${users.active}">deactive</p>
https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#expression-utility-objects
🟡 : 내부 처리 => 컨트롤러 메서드를 통해서 처리
🟨 : 화면 => 뷰를 통해서 처리
static/css/board.css
boardList.html
<link rel="stylesheet" th:href="@{/css/board.css}" />
WoW 예뿌당