for (int i = 0; i < 5; i++) {
score[i] = i * 10;
}
a. 배열의 길이
· 배열의 길이는 양의 정수(0도 포함) 이어야 하며 최대값은 int 타입의 최대값, 약 20억이다
b. 배열이름.length
· 배열은 한번 생성하면 길이를 변경할 수 없기 때문에, 이미 생성된 배열의 길이는 변하지 않는다.
· '배열이름.length'는 상수다.
·for문의 조건식에 배열의 길이를 직접 적어주는 것보다. '배열이름.length'를 사용하는 것이 좋다.
c. 배열의 길이 변경하기
1) 더 큰 배열을 새로 생성
2) 기존 배열의 내용을 새로운 배열에 복사
3) 배열의 초기화
🚀 배열은 생성과 동시에 자동적으로 기본값으로 초기화 됨
➡️ 원하는 값을 저장하려면 각 요소마다 값을 지정해줘야 함
➡️ 배열의 길이가 큰 경우에는 요소 하나하나에 값을 지정하기 보다는 for문을 사용
int score = new int [] { a , b, c, d, e }; // 배열의 생성과 초기화를 동시에 int score = { a , b, c, d, e }; // new int[] 를 생략할 수 있음 (단, 배열의 선언과 생성을 따로하는 경우에는 생략 불가)
int[] score = new int[0]; // 길이가 0인 배열
int[] score = new int[]{}; // 길이가 0인 배열
int[] score = {}; // 길이가 0인 배열, new int [ ] 가 생략됨
4) 배열의 출력
🚀 for문 사용 또는 Arrays.toString(배열이름) 메서드 사용
→ 배열의 모든 요소를 '[첫번째 요소, 두번째 요소, ...]' 와 같은 형식의 문자열로 만들어서 반환
public class Exam01 {
public static void main(String[] args) {
int[] student; // 힙공간의 배열의 주소가 저장될 변수 생성.
student = new int[3]; // 힙공간의 길이가 3인 배열 생성(연속된 주소를 가짐)하고 주소를 반환.
// 정수 배열이 생성이 되고, 초기화를 하지 않으면 자동으로 0으로 초기화.
System.out.println("현재 자동으로 초기화된 값: " + student[0]); // 0
// 인덱스를 이용하여 개별 요소의 값을 변경.
student[0] = 30; // 배열의 첫 번째 요소에 30을 저장
student[1] = 20; // 배열의 두 번째 요소에 20을 저장
student[2] = 10; // 배열의 세 번째 요소에 10을 저장
System.out.println("현재 첫 번째 요소의 값: " + student[0]); // 30
}
}
배열 출력 3가지 방법
📍 for문 출력 | foreach문 출력 | Array.toString() 출력
public class Exam02 {
public static void main(String[] args) {
// for문으로 배열 초기화
int[] c = new int[10];
for(int i = 0; i < c.length; i++) {
c[i] = i;
}
// 배열의 요소를 출력하는 방법 3가지.
// for문을 이용한 출력
System.out.println(c.length);
for (int i = 0; i < c.length; i++) {
System.out.println(c[i]);
}
// foreach 문을 이용한 출력
System.out.println();
for(int d: c) { // (배열의 요소 : 배열).
// 반복시에 변수 d에 순서대로 요소가 들어감. 자바스크립트의 for of문과 유사.
System.out.println(d);
}
// Arrays.toString 메서드 사용. 문자열로 출력.
System.out.println();
System.out.println(Arrays.toString(c));
}
}
배열 사용 X VS. 배열 사용 O
✔️ 학생 5명의 성적 총점과 평균
public class MyArray_01 {
public static void main(String[] args) {
// 1. 배열을 사용하지 않는 경우
int score_001 = 95;
int score_002 = 76;
int score_003 = 67;
int score_004 = 56;
int score_005 = 87;
int total = score_001 + score_002 + score_003 + score_004 + score_005;
double avg = (double)total / 5;
System.out.println("총점은 " + total + "점이고, 평균은 " + avg + "입니다.");
// 2. 배열을 사용하는 경우
// 총점은 for문을 이용해서 구할 수 있고, 배열의 갯수가 늘어난다 해도
// 총점을 구하는 for문과 평균을 구하는 코드를 수정할 필요가 없음
total = 0;
int[] scores = {95, 76, 67, 56, 87}; // 배열 생성
for (int i = 0; i < scores.length; i++) {// scores.length -> 배열의 갯수
total += scores[i];
}
avg = (double)total / scores.length;
System.out.println("총점은 " + total + "점이고, 평균은 " + avg + "입니다.");
}
}
최대값 최소값 구하기
public class MyArray_03 {
public static void main(String[] args) {
/* 최대값, 최소값 구하기 */
int[] score = { 79, 88, 91, 33, 100, 55, 95 };
int max = score[0]; // 배열의 첫 번째 값으로 최대값을 초기화 한다.
int min = score[0]; // 배열의 첫 번째 값으로 최소값을 초기화 한다.
for(int i = 1; i < score.length; i++) {
if(score[i] > max) {
max = score[i];
System.out.println("max: " + max); // 값이 변경되는 것이 안 보이기 때문에 이해가 안될 때 써보기.
}
if(score[i] < min) {
min = score[i];
}
} // end of for
System.out.println("최대값 : " + max);
System.out.println("최소값 : " + min);
}
}
응용 문제
📍 학생 수와 각 학생들의 점수를 입력받아서, 최고 점수 및 평균 점수를 구하는 프로그램
public class Ex_01_Array_03 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
boolean run = true; // 반복문의 조건으로 사용 -> 값이 false가 된다면 반복문이 종료
int studentNum = 0; // 학생수
int[] scores = null; // 점수를 입력 받을 빈 배열 생성. 사용자에게 입력받은 학생수를 기준으로 배열 생성.
while (run) {
System.out.println("-----------------------------------------------------");
System.out.println("1.학생수 | 2.점수입력 | 3.점수리스트 | 4.분석 | 5.종료");
System.out.println("-----------------------------------------------------");
System.out.print("선택> ");
int selectNo = scanner.nextInt();
if (selectNo == 1) { // 학생수를 입력 받아서 배열 생성
System.out.print("학생수> ");
studentNum = scanner.nextInt();
scores = new int[studentNum];
System.out.println(Arrays.toString(scores));
} else if (selectNo == 2) { // 생성된 배열의 갯수 만큼 점수 입력
for(int i = 0; i < scores.length; i++) {
System.out.print("scores["+i+"] : ");
scores[i] = scanner.nextInt();
}
} else if (selectNo == 3) { // 입력받은 배열의 값을 출력
for(int i = 0; i < scores.length; i++) {
System.out.println("score["+i+"] : " + scores[i]);
}
} else if (selectNo == 4) { // 최고 점수, 평균 점수 출력
int max = 0;
int sum = 0;
double avg = 0;
for(int i = 0; i < scores.length; i++) {
max = (max < scores[i]) ? scores[i] : max;
sum += scores[i];
}
avg = (double) sum / studentNum; // 평균
System.out.println("최고 점수: " + max);
System.out.println("평균 점수: " + avg);
} else if (selectNo == 5) { // run 값 변경
run = false;
}
}
System.out.println("프로그램 종료");
scanner.close();
}
}
int i=1; // 초기화
while (조건식) {
// 조건식이 연산결과가 참인 동안, 반복될 문장들을 적는다.
System.out.println(i);
i++; // 증감식
}
💡 먼저 조건식을 평가해서 조건식이 거짓이면 문장 전체를 벗어나고, 참이면 블럭 {} 내의 문장을 수행하고 다시 조건식으로 돌아간다.
➡️ 조건식이 거짓이 될 때까지 이 과정이 계속 반복
💡 조건식에는 비교 또는 논리 연산식이 주로 옴
💡 for문과 while문은 항상 서로 변환이 가능. 단, for문과 달리 조건식 생략 불가!
public class MyWhile_02 {
public static void main(String[] args) {
/* for문을 while문으로 변경 */
int num;
int sum = 0;
for (num=1; num <= 10; num++) {
sum += num;
}
System.out.println("1부터 10까지의 합은 " + sum + "입니다.");
// While문도 초기값, 조건식, 증감식이 존재.
num = 1;
sum = 0;
while(num <= 10) {
sum += num;
num++;
}
System.out.println("1부터 10까지의 합은 " + sum + "입니다.");
}
}
중첩 while문
public class MyWhile_11 {
public static void main(String[] args) {
/* 중첩 while, 구구단 출력 */
int i = 2;
while(i <= 9) {
System.out.println("*** " + i + "단 ***");
int j = 1;
while(j <= 9) {
System.out.println(i +" * "+j+" = " + i*j);
j++;
}
System.out.println();
i++;
} // end of while(i<=9)
}
}
응용 문제
public class Exam022 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String answer = "Y"; // while이 시작될 수 있도록 응답 값을 초기화.
int count = 0;
while (answer.equals("Y")|| answer.equals("y")) {
System.out.println("음악을 재생하시겠습니까?(Y or y)");
answer = scanner.nextLine(); // 사용자의 응답을 받습니다.
answer = answer.toUpperCase(); // 입력받은 문자열을 대문자로 변경.
if (answer.equals("Y") || answer.equals("y")) {
count++;
System.out.println("음악을 "+count+"번 재생했습니다.");
}
}
System.out.println("재생종료");
scanner.close();
}
}
2. do-while 문
💡 while문의 변형으로 기본적인 구조는 while문과 같으나 조건식과 블럭{}의 순서를 바꿔놓은 것
➡️ 블럭{}을 먼저 수행한 후에 조건식을 평가
➡️ while문은 조건식의 결과에 따라 블럭{}이 한 번도 수행되지 않을 수 있지만, do-while문은 최소한 한번은 수행될 것을 보장한다.
💡 실무에서 잘 사용하지는 않음
do {
// 조건식의 연산결과가 참일 때 수행될 문장들을 적는다.
} while (조건식);
public class Exam023 {
public static void main(String[] args) {
/* do~while문은 조건이 거짓이라도 적어도 한번은 문장이 출력된다. */
int num = 100;
do {
System.out.println("적어도 한번은 출력되는 문장");
} while(num < 10);
}
}
응용 문제
public class Ex_03_dowhile_01 {
public static void main(String[] args) {
/* do while 문을 사용해서 사용자로 부터 월의 번호를 입력받는 프로그램을 작성해 보세요.
* 사용자가 올바른 월 번호를 입력할 때 까지 반복을 계속합니다.
* 사용자가 올바른 월 번호를 입력해야만 다음 문장으로 넘어갑니다. */
Scanner scanner = new Scanner(System.in);
int month;
do {
System.out.print("올바른 월을 입력하세요 [1-12] ");
month = scanner.nextInt();
} while (month < 1 || month > 12);
System.out.println("사용자가 입력한 월은 " + month + "입니다.");
scanner.close();
}
}
📄Random random = new Random();
- Random 클래스의 인스턴스를 생성하여 random이라는 변수에 저장하는 코드
- Random :Java에서 무작위 숫자를 생성하기 위한 클래스로, java.util 패키지에 포함되어 있다. 이 클래스는 난수를 생성하는 다양한 메서드를 제공함.
- random() 메소드는 0.0과 1.0 사이의 double 타입 난수를 리턴
⚡️ 0.0 <= Math.random() < 1.0
💡 start 부터 시작하는 n개의 정수 중에서 하나의 정수를 얻기 위한 코드 int num = (int) (Math.random() * n) + start;
public class Ex_03_while_01_pr {
public static void main(String[] args) {
/* while 문과 Random.nextInt() 메소드를 이용해서 2개의 주사위를 던졌을 때 나오는 눈을 (눈1,눈2)
* 형태로 출력하고, 눈의 합이 5가 되는 조합은 (1,4) (4,1) (2,3) (3,2) 입니다.
* 예) (5,1)
* (4,4)
* (6,5)
* (5,2)
* (3,6)
* (1,2)
* (2,3)
*/
Random random = new Random();
while(true) {
int num1 = random.nextInt(6) + 1;
int num2 = random.nextInt(6) + 1;
System.out.println("(" + num1 + ", " + num2 + ")");
if ((num1+num2) == 5) {
break;
}
}
}
}
응용 문제
public class Ex_03_while_02 {
public static void main(String[] args) {
/* 컴퓨터가 주사위를 던지면 사용자가 주사위의 숫자를 맞히는 프로그램을 완성하세요.
사용자가 맞힐 때까지 게임은 계속 됩니다.
예)
주사위 값은 얼마일까요? >>> 5
오답입니다. 다시 시도하세요.
주사위 값은 얼마일까요? >>> 1
1! 정답입니다.
1) break를 사용
2) break를 사용안하는 경우 */
Scanner scanner = new Scanner(System.in);
Random random = new Random();
int answer = random.nextInt(6) + 1;
// System.out.println(answer);
while(true) {
System.out.print("주사위 값은 얼마일까요? >>> ");
int num = scanner.nextInt();
if (answer == num) {
break;
}
System.out.println("오답입니다. 다시 시도하세요.");
}
System.out.println(answer + "! 정답입니다.");
scanner.close();
}
}
3. break 문
💡 자신이 포함된 가장 가까운 반복문을 벗어난다. 주로 if문과 함께 사용되어 특정 조건을 만족하면 반복문을 벗어나도록 한다.
public class Exam024 {
public static void main(String[] args) {
/* 반복문을 빠져나오는 방법은 1) 조건식이 false가 되거나 2) break문이 실행.
* break문은 반복을 멈추게 함.
* 반복문이 진행되다가 break문을 만나면 곧 바로 반복문을 벗어나서 반복문 다음의 코드를 수행. */
Scanner scanner = new Scanner(System.in);
int sum = 0;
int num;
while(true) {
System.out.println("더할 숫자를 입력하세요:(종료하려면 0입력)");
num = scanner.nextInt(); //정수 입력받아서 num에 저장
if (num == 0) { // 만약 0을 입력하였다면 종료.
break;
}
sum += num; //입력받은 값 더해주기.
}
System.out.println("현재까지의 총합 = " + sum);
scanner.close();
}
}
4. continue 문
💡 반복문 내에서만 사용될 수 있으며, 반복이 진행되는 도중에 continue문을 만나면 반복문의 끝으로 이동하여 다음 반복으로 넘어간다.
🔅 break 문➡️반복을 종료, 실행 후 코드 아래의 반복문 코드는 실행되지 않음
🔅continue 문➡️ 다음 반복으로 이동, 실행 후 코드 아래의 반복문 코드는 실행되지 않음
public class MyContinue_01 {
public static void main(String[] args) {
/*
continue문은 현재의 반복을 뛰어넘고 다음 반복을 진행.
반복문이 진행되다가 continue문을 만나면 반복문의 제일 끝으로 가서 다음 반복을 계속 수행.
0 ~ 100까지의 홀수만 더 함
*/
int total = 0;
int num;
for(num=0; num<=100; num++) {
if(num%2 == 0) {
continue;
}
total += num;
}
System.out.println("1부터 100까지의 홀수의 합은 " + total + "입니다.");
}
}
응용 문제
public class Exam025_01 {
public static void main(String[] args) {
/* 한번에 출금할 수 있는 금액은 1000원 이하, break 사용하지 말것 */
// break 사용하지 않는 방법 -> while의 조건식을 이용해서 반복문은 빠져 나올것.
// -> 조건식에 뭘 써야 하나? -> break의 if 조건식은 반복을 종료하는 조건.
// -> while의 조건식은?
Scanner scanner = new Scanner(System.in);
int money = 10000;
System.out.println("현재 가진 돈은 " + money + "원 입니다.");
while(money > 0) {
System.out.print("얼마를 사용하시겠습니까? >>> ");
int spendMoney = scanner.nextInt(); // 정수를 입력받음.
if(spendMoney <= 0 || spendMoney > money) {
System.out.println("다시 입력해 주세요(사용범위가 틀렸습니다.)");
continue; // 현재의 반복을 벗어나서 다시 반복을 시작
} else if (spendMoney > 1000) {
System.out.println("1000원 이상은 출금할 수 없습니다.");
continue;
}
money -= spendMoney;
System.out.println("이제 " + money + "원이 남았습니다.");
}
System.out.println("모든 돈을 사용합니다. 종료");
scanner.close();
}
}
✔️ updateItemImg: 이미지의 원본 파일명, 이미지 파일명, 이미지 URL을 업데이트. ItemImg 객체의 해당 필드들을 수정하는 데 사용됨.
DTO 구현
상품 등록시에는 화면에서 전달받은 DTO 객체를 엔티티 객체로 변환하는 작업을 해야 함
modelmapper 라이브러리 사용하여 DTO(Data Transfer Object)와 엔티티(Entity) 간의 변환을 효율적으로 처리
1. 상품 저장 후 상품 이미지에 대한 데이터를 전달할 DTO 클래스
@Data
public class ItemImgDTO {
private Long id;
private String imgName; // 이미지 파일명
private String oriImgName; // 원본 이미지 파일명
private String imgUrl; // 이미지 조회 경로
private String repImgYn; // 대표 이미지 여부
private static ModelMapper modelMapper = new ModelMapper();
public static ItemImgDTO of(ItemImg itemImg) {
return modelMapper.map(itemImg, ItemImgDTO.class);
}
}
✔️ 멤버 변수로 ModelMapper 객체 추가 ➡️ ItemImg 객체를 파라미터로 받아 ItemImgDTO 로 값을 복사해서 반환
✔️ of 메소드는 주로 컨트롤러에서 데이터 전송을 위해 ItemImgDTO 객체를 생성할 때 사용됨.
✔️ static으로 선언된 of 메소드는 ItemImgDTO 클래스의 인스턴스를 생성하지 않고도 호출할 수 있다
➡️ static으로 선언된 이유는 DTO 변환 로직이 인스턴스의 상태와 관계없이 동작하므로, static 메소드로 정의하여 클래스 수준에서 직접 호출할 수 있도록함. 이로 인해 매핑 로직을 재사용할 수 있고, 코드가 간결해짐.
2. 상품 데이터 정보를 전달하는 DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ItemFormDTO {
private Long id;
@NotBlank(message = "☑️ Item Name is required. ")
private String itemNm;
@NotNull(message = "☑️ Price is required. ")
@Positive(message = "Price must be a positive number.")
private Integer price;
@NotBlank(message = "☑️ Item Detail is required. ")
private String itemDetail;
@NotNull(message = "☑️ Stock Number is required. ")
@Min(value = 0, message = "☑️ Stock Number must be zero or positive.")
private Integer stockNum;
private ItemSellStatus itemSellStatus;
// 상품 저장 후 수정할 때 상품 이미지 정보를 저장하는 리스트
private List<ItemImgDTO> itemImgDTOList = new ArrayList<>();
// 수정 시에 아이디를 담아둘 용도
private List<Long> itemImgIds = new ArrayList<>();
private static ModelMapper modelMapper = new ModelMapper();
// ItemFormDTO의 필드를 Item 엔티티로 매핑
public Item createItem() {
return modelMapper.map(this, Item.class);
}
// Item 엔티티를 ItemFormDTO로 매핑하는 정적 메서드
public static ItemFormDTO of(Item item) {
return modelMapper.map(item, ItemFormDTO.class);
}
}
✔️ 문자열이아닌 Integer에서는 @Length가 아닌 @Min, @Max를 사용
✔️ createItem(): ItemFormDTO 객체의 데이터를 Item 엔티티 객체로 변환. 주로 상품을 데이터베이스에 저장하기 위해 사용
✔️ of(Item item): Item 엔티티 객체를 ItemFormDTO로 변환하는 정적 메소드. 주로 엔티티를 DTO로 변환하여 사용자에게 반환할 때 사용
기존 ItemController 페이지 수정
@Controller
@RequiredArgsConstructor
@Data
public class ItemController {
@GetMapping("/admin/item/new")
public String itemForm(Model model){
model.addAttribute("itemFormDTO", new ItemFormDTO());
return "item/itemForm";
}
}
상품 등록과 같은 관리자 페이지에서 중요한 것은 데이터의 무결성을 보장해야 한다는 것!
데이터가 의도와 다르게 저장되거나 잘못된 값이 저장되지 않도록 validation을 해야 함.
특히 데이터끼리 서로 연관이 있으면 어떤 데이터가 변함에 따라 다른 데이터도 함께 체크를 해야하는 경우 빈번.
* 앞에 memberForm 페이지 참고해서 부트스트랩 코드 작성함..! 아쉬운 점은 부트스트랩 파일 업로드 형식이 너무 제한적.. ㅠ 저런 디자인 말고 다른거 하고 싶은데 버전별로 다른듯..😣
폼 전송할 때 enctype(인코딩 타입) 값으로 'multipart/form-data' 입력 - 웹 폼에서 파일을 업로드할 때 사용되는 인코딩 방식. 이 방식은 데이터와 파일을 서버로 전송할 때 각각의 데이터를 구분하고, 파일을 포함한 복합적인 데이터 전송을 가능하게 함
상품 등록 시
- itemImgDTOList가 비어 있는 경우(새 상품 등록 시) 1~5개의 이미지 업로드 필드를 동적으로 생성.
- itemFormDTO.id가 비어 있을 경우, Register 버튼을 표시
상품 수정 시
- itemImgDTOList가 비어 있지 않은 경우(기존 상품 수정 시) 기존 이미지들을 수정할 수 있도록 이미지 업로드 필드를 생성.
- itemImgIds는 숨겨진 필드로 기존 이미지 ID를 전달.
- itemFormDTO.id가 존재할 경우, Update 버튼을 표시
#lists.isEmpty(...)
•#lists는 Thymeleaf의 내장 객체로, 리스트와 관련된 유틸리티 메소드를 제공
•isEmpty(...)는 #lists 객체의 메소드로, 리스트가 비어 있는지를 확인하는 기능
•작동 방식 : $(this).siblings(".custom-file-label") - 현재 파일 입력 요소의 형제 요소 중 .custom-file-label 클래스를 가진 요소를 선택.
.html(fileName) - 선택된 파일 이름을 해당 요소의 HTML로 설정
application.properties 설정 추가
# 파일 한 개당 최대 사이즈
spring.servlet.multipart.max-file-size=20MB
# 요청당 최대 파일 크기
spring.servlet.multipart.max-request-size=100MB
# 상품 이미지 업로드 경로
itemImgLocation=/Users/Desktop/e-commerce project/item
# 리소스 업로드 경로
uploadPath=file:/Users/Desktop/e-commerce project
WebMvcConfigurer 인터페이스 구현
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${uploadPath}")
String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations(uploadPath);
}
}
addResourceHandlers(ResourceHandlerRegistry registry) : Spring MVC의 리소스 핸들러를 추가하는 메소드. 이 메소드를 통해 리소스(예: 이미지, CSS, JS 파일)의 요청을 처리하는 방법을 설정.
registry.addResourceHandler("/images/**"):/images/**패턴의 URL 요청을 리소스 핸들러로 등록. 즉, 클라이언트가/images/로 시작하는 URL을 요청할 때, 이 핸들러가 요청을 처리함
addResourceLocations(uploadPath) : uploadPath에 설정된 경로에서 리소스를 제공하도록 지정. 이 경로는 파일 시스템의 위치를 나타내며, 이 경로에서 요청된 파일을 제공하게 됨. 예를 들어, /images/example.jpg 요청이 들어오면 uploadPath로 지정된 경로에서 example.jpg 파일을 찾아서 반환.
Spring Framework에서 파일 업로드를 처리하기 위해 사용되는 인터페이스.주로 웹 애플리케이션에서 클라이언트가 서버로 파일을 전송할 때 사용. 이 인터페이스는 파일 업로드와 관련된 다양한 기능을 제공하며, 파일의 메타데이터와 내용을 쉽게 처리할 수 있게 도와줌.
주요 기능
1.파일 정보 접근
•getName(): 파일 필드의 이름을 반환
•getOriginalFilename(): 클라이언트가 업로드한 원본 파일 이름을 반환
•getContentType(): 파일의 MIME 타입을 반환 (예:image/png,text/plain등)
2.파일 내용 처리
•getBytes(): 파일의 내용을 바이트 배열로 반환
•getInputStream(): 파일 내용을InputStream형태로 반환. 이를 통해 파일 내용을 직접 스트리밍하거나 처리할 수 있다.
3.파일 저장
•transferTo(File dest): 파일을 지정된 경로에 저장. 이 메소드는 파일을 실제 파일 시스템에 저장하는 데 사용.
•조건 확인 : oriImgName이 비어있지 않은 경우에만 파일 업로드를 수행
•파일 업로드 : fileService.uploadFile() 메소드를 호출하여 파일을 서버에 업로드. 이 메소드의 결과로 저장된 파일 이름(imgName)을 반환받는다.
•이미지 URL 생성 : 파일의 접근 경로를 기반으로 URL을 생성. 이 URL은 클라이언트가 이미지에 접근하는 데 사용.
•상품 이미지 정보 업데이트 : ItemImg 객체의 updateItemImg 메소드를 호출하여 이미지의 원본 이름, 저장된 파일 이름, URL을 업데이트.
•데이터베이스 저장 : itemImgRepository.save(itemImg)를 호출하여 ItemImg 객체를 데이터베이스에 저장.
ItemService, ItemServiceImpl 구현
public interface ItemService {
Long saveItem(ItemFormDTO itemFormDTO, List<MultipartFile> itemImgFileList)
throws Exception;
}
@Service
@Transactional
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {
private final ItemRepository itemRepository;
private final ItemImgService itemImgService;
private final ItemImgRepository itemImgRepository;
@Override
public Long saveItem(ItemFormDTO itemFormDTO, List<MultipartFile> itemImgFileList) throws Exception {
//상품 등록
Item item = itemFormDTO.createItem();
itemRepository.save(item);
//이미지 등록
for(int i=0; i<itemImgFileList.size(); i++){
ItemImg itemImg = new ItemImg();
itemImg.setItem(item);
if(i == 0)
itemImg.setRepImgYn("Y");
else
itemImg.setRepImgYn("N");
itemImgService.saveItemImg(itemImg, itemImgFileList.get(i));
}
return item.getId();
}
}
itemRepository.save(item)를 호출하여 Item 객체를 데이터베이스에 저장
itemImgFileList에 있는 모든 이미지 파일에 대해 반복.
ItemImg객체 생성 후itemImg.setItem(item)을 호출하여 현재 Item 객체와 ItemImg 객체를 연결
첫 번째 이미지 파일 (i == 0)은 대표 이미지로 설정 (itemImg.setRepImgYn("Y"))
itemImgService.saveItemImg(itemImg, itemImgFileList.get(i))를 호출하여 ItemImg 객체와 이미지 파일을 저장
저장된 Item 객체의 ID를 반환
List<MultipartFile> itemImgFileList는 클라이언트에서 전송된 파일들로 구성된 리스트로, MultipartFile 객체는 Spring MVC가 파일 업로드 요청을 처리할 때 자동으로 생성. 이 리스트는 컨트롤러 메소드에서 @RequestParam을 사용하여 수신되며, 이후 서비스 계층에서 파일을 저장하거나 처리하는 데 사용.
ItemController 추가 코드
private final ItemService itemService;
@PostMapping("/admin/item/new")
public String itemNew(@Valid ItemFormDTO itemFormDTO, BindingResult bindingResult,
Model model, @RequestParam("itemImgFile") List<MultipartFile> itemImgFileList){
if(bindingResult.hasErrors()){
return "item/itemForm";
}
if(itemImgFileList.get(0).isEmpty() && itemFormDTO.getId() == null){
model.addAttribute("errorMessage", "첫번째 상품 이미지는 필수 입력 값 입니다.");
return "item/itemForm";
}
try {
itemService.saveItem(itemFormDTO, itemImgFileList);
} catch (Exception e){
model.addAttribute("errorMessage", "상품 등록 중 에러가 발생하였습니다.");
return "item/itemForm";
}
return "redirect:/";
}
테스트코드 작성
ItemImgRepository 인터페이스에 findByItemOrderByIdAsc 메소드 추가
MockMultipartFile : Spring의 MockMultipartFile 클래스는 테스트 중 파일 업로드를 시뮬레이션하는 데 사용된다. 실제 파일이 아닌 가상의 파일을 생성할 수 있음. path, imageName, contentType, content를 매개변수로 받아 파일의 메타데이터와 내용을 설정함.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/h2-console/**").permitAll() // H2 콘솔에 대한 접근 허용
.anyRequest().authenticated() // 나머지 요청에 대해 인증 필요
);
// use HTTP Basic authentication
http.httpBasic(Customizer.withDefaults());
// disable Cross Site Request Forgery(CSRF)
http.csrf(csrf -> csrf.disable());
return http.build();
}
// H2 콘솔 관련 설정
@Bean
@ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true")
public WebSecurityCustomizer configureH2ConsoleEnable() {
return web -> web.ignoring()
.requestMatchers(PathRequest.toH2Console());
}
@Configuration : Spring의 구성 클래스를 나타내며, Bean 정의를 포함하는 클래스를 의미
@EnableWebSecurity : Spring Security를 활성화. Spring Security의 설정을 정의하고 적용
💡 BCryptPasswordEncoder 비밀번호를 암호화하고 검증하는 데 사용되는 PasswordEncoder 구현체. BCrypt 알고리즘을 사용하여 비밀번호를 암호화한다. BCrypt는 비밀번호 해시를 생성하는 데 안전한 방법으로 널리 사용
SecurityFilterChain : HTTP 요청에 대한 보안 필터 체인을 정의. HttpSecurity 객체를 사용하여 필터 체인을 설정.
HttpSecurity : HTTP 요청에 대한 보안 설정을 구성하는 데 사용됨
authorizeHttpRequests(configurer -> ...) : HTTP 요청에 대한 접근 권한을 설정
requestMatchers(...) : URL 패턴에 따라 요청을 필터링.
permitAll() : 해당 URL 패턴에 대해 인증 없이 접근을 허용.
anyRequest().authenticated() : 위에서 정의된 패턴을 제외한 모든 요청은 인증된 사용자만 접근할 수 있도록 설정.
httpBasic(Customizer.withDefaults()) : HTTP Basic Authentication을 활성화. 이 방법은 간단한 인증 방법으로 사용자 이름과 비밀번호를 HTTP 헤더에 포함시켜 인증을 수행.
csrf.disable() : Cross Site Request Forgery (CSRF) 보호를 비활성화. CSRF는 웹 애플리케이션에서 악의적인 요청을 방지하기 위한 보안 메커니즘. 일반적으로 비활성화하지 않는 것이 좋지만, API 또는 비상 상황에서는 비활성화할 수 있다.
회원가입 폼에서 받을 내용 - 이름 | 이메일 주소 (로그인 시 확인) | 비밀번호 | 주소
message는 유효성 검사시 에러가 발생하면 출력될 내용!
3. Member 클래스 코드 - 회원 정보를 저장하는 엔티티 (DB에서 관리)
@Entity
@Table(name = "member")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue (strategy = GenerationType.IDENTITY)
@Column(name="member_id")
private Long memberId;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String address;
@Enumerated(EnumType.STRING)
private Role role;
public static Member createMember(MemberFormDTO memberFormDTO,
PasswordEncoder passwordEncoder) {
String password = passwordEncoder.encode(memberFormDTO.getPassword());
Member member = Member.builder()
.name(memberFormDTO.getName())
.email(memberFormDTO.getEmail())
.password(password)
.address(memberFormDTO.getAddress())
.role(Role.USER)
.build();
return member;
}
}
데이터베이스 테이블 이름은 'member'로 지정
이메일을 통해 회원 구분을 위해 'unique'설정
Role은 string으로 저장하도록 설정
Member 엔티티 생성하는 메소드 설정 - 파라미터로 dto와 passwordencoder를 받음
패스워드는 db에 암호화 돼서 저장됨
role은 'USER'로 설정해줌 -> ADMIN 먼저 만들고 바꿔줘도 됨
Repository 코드 설정 - DAO 역할
repository 패키지 아래 MemberRepository 인터페이스 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmail(String email);
}
회원가입 시 중복된 회원이 있는지 검사하기 위해 이메일로 회원을 검사할 수 있도록 쿼리 메소드 작성
Service 코드 설정
service 패키지 아래 MemberService 인터페이스, MemberServiceImpl 클래스 생성
특성
서비스 계층이 필요한 경우
서비스 계층이 필요없는 경우
비즈니스 로직의 복잡성
복잡한 비즈니스 로직이 있는 경우
비즈니스 로직이 거의 없거나 단순한 경우
유지보수
비즈니스 로직을 중앙화하여 유지 보수 용이
간단한 애플리케이션에서 유지 보수가 덜 필요
테스트 용이성
독립적인 테스트가 필요한 경우
테스트가 크게 중요하지 않은 경우
트랙잭션 관리
트랜잭션 관리를 필요로 하는 경우
트랜잭션 관리가 필요 없는 경우
계층화된 아키텍처
MVC 패턴 등 계층화된 아키텍처를 선호하는 경우
단일 클래스 또는 구조화된 아키텍처가 불필요
public interface MemberService {
Member saveMember(Member theMember);
}
@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService, UserDetailsService {
private final MemberRepository memberRepository;
@Override
public Member saveMember(Member theMember) {
Member tempMember = memberRepository.findByEmail(theMember.getEmail());
if (tempMember != null) {
throw new IllegalStateException("Already Registered Member!");
}
return memberRepository.save(theMember);
}
}
특성
단일 클래스
인터페이스 + 구현체
구조
단순하지만 커질 수 있음
명확하게 구조화됨
유지보수
어려울 수 있음
용이함
테스트
어려울 수 있음
용이함 (모킹 가능)
확장성
낮음
높음
의존성 주입
어려울 수 있음
쉬움
@Autowired는 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록 가능하면 사용 안해도 의존성 주입 가능..
이미 가입된 회원은 IllegalStateException 예외 발생
회원 가입 기능 테스트
마우스 오른쪽 버튼 - Go To - Test 자동생성 기능 사용
@SpringBootTest
@Transactional
@Log4j2
@RequiredArgsConstructor
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberServiceImplTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
@Test
@DisplayName("회원가입 테스트")
public void saveMemberTest() {
MemberFormDTO theMemberDTO = MemberFormDTO.builder()
.email("test@email.com")
.name("Dora")
.address("Daegu")
.password("1234")
.build();
// 비밀번호 인코딩 및 회원 생성
Member member = Member.createMember(theMemberDTO, passwordEncoder);
// 회원 저장
Member savedMember = memberService.saveMember(member);
// 로그로 저장된 Member 확인
log.info("Saved Member: {}", savedMember);
assertNotNull(savedMember.getMemberId()); // ID가 null이 아닌지 확인
assertEquals("test@email.com", savedMember.getEmail());
}
@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMember() {
// 객체 생성
Member member1 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", ADMIN);
Member member2 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", USER);
// 객체 저장
memberService.saveMember(member1);
// 예외 처리
IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> {
memberService.saveMember(member2);
});
assertTrue(thrown.getMessage().contains("Already Registered Member!"));
}
}
h2 db에 테스트 할 거기 때문에 기존 properties 외 새로 하나 더 만들어서 경로를 지정해줬다.
Junit의 Assertions 클래스의 assertEquals 메소드를 이용해서 저장하려고 요청했던 값과 실제 저장된 데이터를 비교함.