@Component
public class Messenger {
public void sendGreeting(String msg) {
System.out.println("오늘의 인사 : "+msg);
}
public void sendMember(MemberDto dto) {
System.out.println(dto);
}
public String getMessage() {
System.out.println("getMessage() 메소드가 수행됩니다");
return "열심히 공부 합시다";
}
}
testAop 메소드에 작성@Autowired
private Messenger m;
public void testAop() {
m.sendGreeting("안녕하세요");
m.sendMember(null);
String msg = m.getMessage();
System.out.println("리턴된 msg : "+msg);
}
오늘의 인사 : 안녕하세요
null
getMessage() 메소드가 수행됩니다
리턴된 msg : 열심히 공부 합시다
@Before + @After 의 개념
@Around("execution(void send*(..))") : 매개변수의 모양을 상관하지 않겠다getArgs() 사용@Aspect
@Component
public class MessengerAspect{
@Around("execution(void send*(..))") // @Before + @After 개념이라고 생각
public void checkGreeting(ProceedingJoinPoint joinPoint) {
// 메소드에 전달된 인자들 목록을 얻어내기
Object[] args = joinPoint.getArgs();
// 반복문 돌면서 매개변수에 담긴 값들을 하나하나 참조하면서
for(Object tmp : args) {
// 찾고 싶은 type 을 확인
if(tmp instanceof String) { // 만일 String type 이라면
// 찾았다면 원래 type 으로 casting 한다
String msg = (String)tmp;
System.out.println("매개변수에 전달된 값 : "+msg);
}
}
}
}
매개변수에 전달된 값 : 안녕하세요
➜ 메소드 호출 안 했다
proceed()joinPoint.proceed();
로그 출력을 도와주는 어노테이션
해당 어노테이션이 붙어있는 클래스의 메소드에서 log 객체를 사용 가능
@Slf4j
// log 객체를 이용해서 메세지를 출력
log.info("매개변수에 전달된 값 : " +msg);
log.warn("매개변수에 전달된 값 : " +msg);
log.error("매개변수에 전달된 값 : " +msg);
log.debug("매개변수에 전달된 값 : " +msg);
[2m2025-08-28T10:44:26.189+09:00[0;39m [32m INFO[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : 안녕하세요
[2m2025-08-28T10:46:12.407+09:00[0;39m [33m WARN[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : 안녕하세요
2025-08-28T10:47:25.011+09:00[0;39m [31mERROR[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : 안녕하세요
application.properties 에 debug 관련 코드 작성
이 프로젝트의 패키지 안에서 일어나는 debug 로그만 출력하겠다는 의미
logging.level.com.example.spring08=DEBUG
log.debug 로 수정@Slf4j
@Component
public class Messenger {
public void sendGreeting(String msg) {
log.debug("오늘의 인사 : "+msg);
}
public void sendMember(MemberDto dto) {
if(dto != null) {
log.debug(dto.toString());
}
}
public String getMessage() {
log.debug("getMessage() 메소드가 수행됩니다");
return "열심히 공부 합시다";
}
}
log.debug 로 수정log.debug("리턴된 msg : "+msg);
[2m2025-08-28T11:02:04.831+09:00[0;39m [32mDEBUG[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : 안녕하세요
[2m2025-08-28T11:02:04.831+09:00[0;39m [32mDEBUG[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mcom.example.spring08.aop.Messenger [0;39m [2m:[0;39m 오늘의 인사 : 안녕하세요
[2m2025-08-28T11:02:04.831+09:00[0;39m [32mDEBUG[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mcom.example.spring08.aop.Messenger [0;39m [2m:[0;39m getMessage() 메소드가 수행됩니다
[2m2025-08-28T11:02:04.831+09:00[0;39m [32mDEBUG[0;39m [35m15984[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.e.spring08.Spring08ExampleApplication [0;39m [2m:[0;39m 리턴된 msg : 열심히 공부 합시다
MemberDto 에 어노테이션 추가
@Data : setter, getter 메소드를 만들어주고 toString() 메소드를 오버라이딩 해서 필드에 들어 있는 정보를 확인 가능
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Alias("memberDto")
@Data
public class MemberDto {
private int num;
private String name;
private String addr;
}
MemberDto dto = MemberDto.builder().num(1).name("유재석").addr("압구정").build();
m.sendMember(dto);
if(tmp instanceof MemberDto) { // 만일 MemberDto type 이라면
log.debug("매개변수에 전달된 값 : "+tmp);
MemberDto dto = (MemberDto)tmp;
dto.setName("유제니");
}
[2m2025-08-28T11:18:29.645+09:00[0;39m [32mDEBUG[0;39m [35m11400[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mcom.example.spring08.aop.Messenger [0;39m [2m:[0;39m 오늘의 인사 : 안녕하세요
[2m2025-08-28T11:18:29.649+09:00[0;39m [32mDEBUG[0;39m [35m11400[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : MemberDto(num=1, name=유재석, addr=압구정)
[2m2025-08-28T11:18:29.649+09:00[0;39m [32mDEBUG[0;39m [35m11400[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mcom.example.spring08.aop.Messenger [0;39m [2m:[0;39m MemberDto(num=1, name=유제니, addr=압구정)
log.debug("매개변수에 전달된 값 : " +msg);
if(msg.contains("박명수")) {
log.error("박명수는 금지된 사용자의 이름으로 차단합니다");
return; // 여기서 리턴하면 아래의 joinPoint.proceed(); 가 호출이 안된다
}
// 오늘의 인사가 출력 안된다
m.sendGreeting("안녕하세요, 박명수씨");
// 오늘의 인사가 출력된다
m.sendGreeting("안녕하세요");
매개변수에 전달된 값 : 안녕하세요, 박명수씨
[2m2025-08-28T11:41:09.870+09:00[0;39m [32mDEBUG[0;39m [35m16864[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : 안녕하세요, 박명수씨
[2m2025-08-28T11:41:09.870+09:00[0;39m [31mERROR[0;39m [35m16864[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 박명수는 금지된 사용자의 이름으로 차단합니다
매개변수에 전달된 값 : 안녕하세요
[2m2025-08-28T11:41:09.870+09:00[0;39m [32mDEBUG[0;39m [35m16864[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : 안녕하세요
[2m2025-08-28T11:41:09.870+09:00[0;39m [32mDEBUG[0;39m [35m16864[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mcom.example.spring08.aop.Messenger [0;39m [2m:[0;39m 오늘의 인사 : 안녕하세요
[2m2025-08-28T11:41:09.871+09:00[0;39m [32mDEBUG[0;39m [35m16864[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 매개변수에 전달된 값 : MemberDto(num=1, name=유재석, addr=압구정)
[2m2025-08-28T11:41:09.871+09:00[0;39m [32mDEBUG[0;39m [35m16864[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mcom.example.spring08.aop.Messenger [0;39m [2m:[0;39m MemberDto(num=1, name=유제니, addr=압구정)
checkReturn 메소드 생성String 이고com.example.spring08.aop 패키지에 속해있는 모든 클래스(*) 중에서get 으로 시작하는 메소드aspect 가 허용@Around("execution(String com.example.spring08.aop.*.get*())")
public Object checkReturn(ProceedingJoinPoint joinPoint) throws Throwable {
// aspect 가 적용된 메소드를 실행하고 해당 메소드가 리턴하는 값을 변수에 담기
Object obj = joinPoint.proceed();
// 원래 type 으로 casting
String returnValue = (String)obj;
log.debug("원래 리턴한 값 : "+returnValue);
// 리턴값이 있는 메소드에 aspect 를 적용하면 반드시 해당 데이터를 리턴해야 한다
return "공부하지 말고 놉시다"; // 다른 값을 리턴해줄 수도 있다
}
[2m2025-08-28T11:59:51.025+09:00[0;39m [32mDEBUG[0;39m [35m9468[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.example.spring08.aop.MessengerAspect [0;39m [2m:[0;39m 원래 리턴한 값 : 열심히 공부 합시다
[2m2025-08-28T11:59:51.025+09:00[0;39m [32mDEBUG[0;39m [35m9468[0;39m [2m--- [Spring08_Example] [ restartedMain] [0;39m[36mc.e.spring08.Spring08ExampleApplication [0;39m [2m:[0;39m 리턴된 msg : 공부하지 말고 놉시다
DI (Dependency Injection)
IOC (Inversion Of Control)
AOP (Aspect Oriented Programming)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GalleryDto {
private int num;
private String title;
private String writer;
private String content;
private String createdAt;
private List<GalleryImageDto> imageList;
private String profileImage;
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GalleryImageDto {
private int num;
private int galleryNum;
private String saveFileName;
private String createdAt;
}
insert 생성insertImage 생성getSequence 생성getData 생성getListWithImages 생성getImageList 문 생성@Mapper
public interface GalleryMapper {
// 게시글 저장
@Insert("""
INSERT INTO gallery (num, title, writer, content)
VALUES (#{num}, #{title}, #{writer}, #{content})
""")
public void insert(GalleryDto dto);
// 게시글 이미지 저장
@Insert("""
INSERT INTO gallery_image (num, galleryNum, saveFileName)
VALUES (gallery_image_seq.NEXTVAL, #{galleryNum}, #{saveFileName})
""")
public int insertImage(GalleryImageDto dto);
@Select("SELECT board_seq.NEXTVAL AS num FROM DUAL")
public int getSequence();
@Select("""
SELECT g.num, title, writer, content,
TO_CHAR(g.createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt,
profileImage
FROM gallery g
JOIN users u ON writer = userName
WHERE g.num = #{num}
""")
public GalleryDto getData(int num);
public List<GalleryDto> getListWithImages(); // <select id="getListWithImage">
@Select("""
SELECT num, saveFileName, TO_CHAR(createdAt, 'YYYY-MM-DD HH24:MI:SS') AS createdAt
FROM gallery_image
WHERE galleryNum = #{num}
ORDER BY num ASC
""")
public List<GalleryImageDto> getImageList(int num);
}
getListWithImages 문 생성galleryImageMap 문 생성property = "Dto 의 필드명" column = "SELECT 된 칼럼명"galleryMap 문 생성List<GalleryImageDto> type 이기 때문에 collection 요소를 이용해서 galleryImageMap 의 결과를 넣도록 설정한다<mapper namespace="com.example.spring08.repository.GalleryMapper">
<!-- GalleryImageDto 맵핑 -->
<resultMap id="galleryImageMap" type="com.example.spring08.dto.GalleryImageDto">
<id property="num" column="gi_num"/>
<result property="galleryNum" column="galleryNum"/>
<result property="saveFileName" column="saveFileName"/>
<result property="createdAt" column="gi_createdAt"/>
</resultMap>
<!-- GalleryDto 맵핑 -->
<resultMap id="galleryMap" type="com.example.spring08.dto.GalleryDto">
<id property="num" column="g_num"/>
<id property="writer" column="writer"/>
<id property="title" column="title"/>
<id property="content" column="content"/>
<id property="createdAt" column="g_createdAt"/>
<collection property="imageList" resultMap="galleryImageMap"/>
</resultMap>
<select id="getListWithImages" resultMap="galleryMap">
SELECT g.num AS g_num, g.writer, g.title, g.content,
TO_CHAR(g.createdAt, 'YYYY-MM-DD HH24:MI:SS') AS g_createdAt,
gi.galleryNum, gi.num AS gi_num, gi.saveFileName,
TO_CHAR(gi.createdAt, 'YYYY-MM-DD HH24:MI:SS') AS gi_createdAt
FROM gallery g
JOIN gallery_image gi
ON g.num = gi.galleryNum
ORDER BY g.num DESC, gi.num ASC
</select>
</mapper>
galleryList 메소드 생성@RequiredArgsConstructor
@Controller
public class GalleryController {
@GetMapping("/gallery/list")
public String galleryList(Model model) {
return "gallery/list";
}
}
public interface GalleryService {
public List<GalleryDto> getGalleryList();
}
@RequiredArgsConstructor
@Service
public class GalleryServiceImpl implements GalleryService{
// 의존 객체를 생성자로 주입 받기
private final GalleryMapper galleryMapper;
@Override
public List<GalleryDto> getGalleryList() {
return galleryMapper.getListWithImages();
}
}
galleryList 메소드 완성@RequiredArgsConstructor
@Controller
public class GalleryController {
private final GalleryService galleryService;
@GetMapping("/gallery/list")
public String galleryList(Model model) {
// 응답에 필요한 객체를 "list" 라는 키값으로 담고
model.addAttribute("list", galleryService.getGalleryList());
// Thymeleaf 페이지에서 목록을 응답
return "gallery/list";
}
}
<th:block th:insert="~{/include/navbar :: nav('board')}"></th:block>
<div class="container">
<a class="btn btn-outline-primary btn-sm mt-2" th:href="@{/gallery/new-form}">
<i class="bi bi-image"></i>
<i class="bi bi-plus"></i>
<span class="visually-hidden">겔러리 새글 작성</span>
</a>
<h1>Gallery 목록</h1>
<div class="row" id="masonry-grid">
<div th:each="tmp : ${list}" class="col-sm-6 col-md-4 col-lg-3 mb-4">
<div class="card">
<img class="card-img-top"
th:src="@{/upload/{name}(name=${tmp.imageList.get(0).saveFileName})}"/>
<div class="card-body">
<h4 th:text="${tmp.title}"></h4>
<a class="btn btn-sm btn-success"
th:href="@{/gallery/view(num=${tmp.num})}">
<span th:text="${#lists.size(tmp.imageList)}"></span> 개의 이미지
<!-- tmp.imageList.size() 도 가능하지만 null 인 경우 오류 발생-->
</a>
</div>
</div>
</div>
</div>
</div>
<script>
// masonry 레이아웃을 초기화 하는 javascript
const grid = document.querySelector("#masonry-grid");
imagesLoaded(grid, ()=>{
new Masonry(grid, { percentPosition:true});
});
</script>
String[] whiteList = {"/", "/user/loginform", "/user/login-fail",
"/user/expired", "/user/signup-form", "/user/signup",
"/test/**", "/user/check-id", "/board/list", "/board/view",
"/upload/**", "/gallery/list", "/gallery/view"}
newForm 메소드 작성@GetMapping("/gallery/new-form")
public String newForm() {
return "gallery/new-form";
}
<style>
.drop-zone {
border: 2px dashed #0d6efd;
cursor: pointer;
border-radius: 10px;
background-color: #f8f9fa;
display:flex;
align-items: center;
justify-content: center;
height: 200px;
}
/* drop zone 에 파일을 드레그해서 위에 올렸을때 적용할 css */
.drop-zone.dragover {
background-color: #e9ecef;
}
.preview img {
max-width: 120px;
margin: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.preview-item {
position: relative;
display: inline-block;
}
.remove-btn {
position: absolute;
top: 11px;
right: 11px;
background-color: #ffffffcc; /* 반투명 흰색 */
color: #333;
border: none;
border-radius: 4px;
width: 20px;
height: 20px;
font-size: 16px;
cursor: pointer;
text-align: center;
transition: background-color 0.2s, color 0.2s;
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
}
.remove-btn:hover {
background-color: #dc3545;
color: white;
}
</style>
<div class="container mt-4">
<h2><i class="bi bi-image"></i> 새 갤러리 작성</h2>
<form id="saveForm" th:action="@{/gallery/save}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="title" class="form-label">제목</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea class="form-control" id="content" name="content" rows="6" required></textarea>
</div>
<div class="mb-3">
<label class="form-label">이미지 업로드 (Drag & Drop 또는 클릭)</label>
<div class="drop-zone" id="dropZone">
이곳에 이미지를 끌어다 놓거나 클릭하여 선택하세요.
<input type="file" name="images" id="fileInput" multiple hidden>
</div>
<div class="preview d-flex flex-wrap mt-3" id="preview"></div>
</div>
<button type="submit" class="btn btn-primary">등록</button>
</form>
</div>
<script>
const dropZone = document.querySelector("#dropZone");
const fileInput = document.querySelector("#fileInput");
const preview = document.querySelector("#preview");
let selectedFiles = [];
// drop zone 을 클릭했을때 input type="file" 을 강제 클릭해서 파일 선택창이 열리도록 한다
dropZone.addEventListener("click", () => fileInput.click());
// drop zone 에 마우스가 드레그해서 들어갔을때
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
//dragover 클래스를 추가해서 회색으로 변경되도록 한다.
dropZone.classList.add("dragover");
});
// drop zone 에 마우스가 드레그해서 나갔을때
dropZone.addEventListener("dragleave", () => {
//dragover 클래스를 제거해서 색상이 원래 색으로 돌아가도록 한다.
dropZone.classList.remove("dragover");
});
//파일을 끌어다가 drop 했을때
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
//선택된 파일객체가 들어 있는 배열을 기능을 모두 쓸수 있는 배열로 변경한다음
const files = Array.from(e.dataTransfer.files);
//기존에 선택한 파일이 들어 있는 배열에 합친다
selectedFiles = [...selectedFiles, ...files];
//preview 를 업데이트한다(미리보기 기능)
updatePreview();
});
// drag drop 이 아니고 파일을 직접 선택했을때 input type="file" 에 change 이벤트가 발생한다.
fileInput.addEventListener("change", () => {
//위와 동일한 동작을 한다
const files = Array.from(fileInput.files);
selectedFiles = selectedFiles.concat(files);
updatePreview();
});
// preview 을 업데이트 하는 함수 (내부에서 await 이 있는 비동기 함수이기 때문에 async 키워드를 붙여서 만들었다 )
async function updatePreview() {
//이미 출력된 이미지를 모두 없애고
preview.innerHTML = "";
// input type="file" 에 value 를 넣어줄 정보를 구성하기 위한 객체
const dataTransfer = new DataTransfer();
//반복문 돌면서 선택된 파일 객체를 순서대로 참조하면서
for (let i = 0; i < selectedFiles.length; i++) {
// i 번째 파일 객체를 불러와서
const file = selectedFiles[i];
//만일 이미지 파일이 아니라면 continue (for 문을 계속 실행한다 continue 아래의 코드를 실행하지 않고 )
if (!file.type.startsWith("image/")) continue;
try {
// 선택한 이미지 파일을 읽어 들여서 결과를 얻어낸다
const imageUrl = await readFileAsDataURL(file);
const container = document.createElement("div");
container.classList.add("preview-item");
const img = document.createElement("img");
img.setAttribute("src", imageUrl);
const btn = document.createElement("button");
btn.classList.add("remove-btn");
btn.innerText = "×";
btn.addEventListener("click", ()=>{
// selectedFiles 배열에서 i 번째방으로 부터 1개의 item 삭제
selectedFiles.splice(i, 1);
//삭제후 preview 가 다시 update 되도록 함수를 다시 호출해 준다.
updatePreview();
});
container.insertAdjacentElement("beforeend", img);
container.insertAdjacentElement("beforeend", btn);
preview.insertAdjacentElement("beforeend", container);
//DataTransfer 객체에 파일객체를 추가
dataTransfer.items.add(file);
} catch (err) {
console.error("이미지 로딩 실패:", err);
}
}
//선택한 파일(이미지 파일만 선별)객체가 들어있는 배열을 input type="file" 의 value 로 넣어준다.
fileInput.files = dataTransfer.files;
}
//전달된 파일을 읽어들이는 Promise 객체를 리턴해주는 함수
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
//FileReader객체를 이용해서 파일을 읽어들이고
const reader = new FileReader();
//모두다 읽었을때 실행할 함수 등록
reader.onload = ()=>{
//읽은 data url 문자열을 resolve() 함수를 호출하면서 전달한다(Promise 가 해결된다)
resolve(reader.result);
}
reader.readAsDataURL(file);
});
}
//폼에 "submit" 이벤트가 일어 났을때 실행할 함수 등록
document.querySelector("#saveForm").addEventListener("submit", (e)=>{
//여기서 폼 입력란에 대한 검증을 하고 폼 제출을 막고 싶으면 e.preventDefault() 를 실행하게 하면 된다.
//만일 선택한 파일이 없다면 폼 전송 막기
if(selectedFiles.length < 1){
alert("업로드할 이미지를 1 개 이상 선택해 주세요");
e.preventDefault();
}
});
</script>
public class GalleryUploadRequest {
private String title;
private String content;
// <input type="file" name="images" multiple>
// images 라는 파라미터명으로 파일이 여러개 전송되니까 MultipartFile[] type 으로 선언한다
private MultipartFile[] images;
}
GalleryController 에 gallerySave 메소드 생성
업로드된 파일의 모든 정보를 GalleryUploadRequest 의 images 라는 MultipartFile[] 객체에 담아서 전달
@PostMapping("/gallery/save")
public String gallerySave(GalleryUploadRequest uploadRequest) {
return "gallery/save";
}