1.  배열의 활용 

1) 섞기 (shuffle)  -  배열의 요소의 순서를 바꾼다 (ex. 카드섞기, 로또번호생성)

 

  1)  길이가 10인 배열을 생성, 0~9의 숫자로 차례대로 초기화하여 출력

  2)  random()을 이용해서 배열의 임의의 위치에 있는 값과 배열의 첫 번째 요소인 number[0]의 값을 교환하는 일을 반복

        ❗️ number[0]과 number[n]에 저장된 값을 서로 바꾸려면 별도의 저장공간이 하나 더 필요

 

// 별도 공간 생성 방법
int tmp = number [0];
number [0] = number [n];
nuber [n] = tmp;

 

public class MyArray_05 {

	public static void main(String[] args) {
		int[] number = new int[10];
		
		for(int i = 0; i < number.length; i++) {
			number[i] = i; // 배열을 0 ~ 9의 숫자로 초기화한다.
		    System.out.print(number[i] + " "); // 0 1 2 3 4 5 6 7 8 9
		}
		System.out.println();
		
		for(int i = 0; i < 10000; i++) {
			int n = (int)(Math.random() * 10); // 0~9중의 한 값을 임의로 얻는다.
			
			// swap
			int temp = number[0];
			number[0] = number[n];
			number[n] = temp;
		}

		for (int i = 0; i < number.length; i++) {
			System.out.print(number[i] + " ");  // 2 4 8 7 6 0 5 9 1 3 
		}
	}
}
 
로또번호 생성하기 

 

  1)  길이가 45인 배열에 1부터 45까지의 값을 담는다

  2)  반복문을 이용해서 배열의 인덱스가 i인 값과 random()에 의해서 결정된 임의의 위치에 있는 값과 자리를 바꾸는 것을 6번 반복한다.

       ❗️임의의 값으로 배열 채우기

public class MyArray_05_01 {

	public static void main(String[] args) {
		int[] number = new int[45];
		
		for(int i = 0; i < number.length; i++) {
			number[i] = i+1; 
		    System.out.print(number[i] + " "); 
		}
	    
        System.out.println();
	    for(int i =0; i < 10000; i++) {
	    	int n = (int)(Math.random() * 45);
	    	
	    	// swap
	    	int temp = number[0];
	    	number[0] = number[n];
	    	number[n] = temp;
	    }

	    for (int i = 0; i < 6; i++) {
	    	System.out.print(number[i] + " ");
	    }
	}
}

로또 중복값 제거
public class Ex_01_Array_04 {

	public static void main(String[] args) {

		int[] numbers = new int[6];
        
		for(int i = 0; i < numbers.length; i++) {
			int temp = (int)(Math.random() * 45) + 1; // 1 ~ 45의 임의의 값을 얻는다.
			
			// 기존에 값이 있는지 확인
			boolean isResult = true; 
			for(int j = 0; j < i; j++) {
            
			    if (numbers[j] == temp) {
				    System.out.println(temp + "은 중복된 값입니다.");
				    isResult = false;
				    break;
				}
			} 
			
			// 중복이 없으면 저장, 중복이 있으면 i값 1감소
			if (isResult) {
				numbers[i] = temp;
			}
			else {
				i--;
			}
		}
        System.out.println(Arrays.toString(numbers));
	}
}
 
 

2.  2차원 배열

타입[][] 배열이름 ;
ex. int [][] scores = new int [3] [3] ;

 

🚀  2차원 배열의 선언 방법은 1차원 배열을 선언하는 부분에 괄호[]를 하나 더 추가

🚀  2차원 배열은 행과 열로 구성되어 있으며 첫 번째 괄호의 인덱스가 행을, 두 번째 괄호의 인덱스가 열을 뜻함

       ➡️ 위의 예제를 풀어보면 3행 3열을 뜻함. (3x3 = 총 9개의 방이 생김)

🚀  배열을 순회하려면 for문이 2개 필요

 

public class MyArray_07 {

	public static void main(String[] args) {
		/* 2차원 배열 선언 및 출력 */
	    int[][] arr = {
	    		{1, 2, 3}, 
	    		{4, 5, 6}
	    }; // 2차원 배열 선언
		
		for(int i = 0; i < arr.length; i++) {
			for(int j = 0; j < arr[i].length; j++) {
				System.out.print(arr[i][j] + " ");			
				}
			System.out.println();
		}
		
		System.out.println("행: " + arr.length); // 2 (길이는 행만 불러옴)
		System.out.println("열: " + arr[0].length); // 3 (0행의 칸이 몇개인지를 불러옴)

	}

}

 

 

응용 문제

 

  📍 중첩 for문을 이용해서 주어진 배열의 전체 항목의 합과 평균값을 구하세요

public class Ex_02_02 {

	public static void main(String[] args) {

		int[][] array = {
                {95, 86},
                {83, 92, 96},
                {78, 83, 93, 87, 88}
        };
		
		int sum = 0;
		double avg = 0.0;
		int count = 0;
		
		for(int i = 0; i < array.length; i++) {
			for(int j = 0; j < array[i].length; j++) {
				sum += array[i][j];
				count++;
			}
		}
		avg = (double) sum / count;
		
		System.out.println("sum: " + sum); // 881
		System.out.println("avg: " + avg); // 88.1
	}
}

 

 

 

 

* 내용참고 - 수업 강의 및 자바의 정석

 


배열 (Array)

💡 같은 타입여러 변수를 하나의 묶음으로 다루는 것

💡 String[] classGroup = { "김철수", "김영희", "한소희",...}

       ➡️  classGroup이라는 변수에 문자열로 된 배열이 담긴다.

 

1)  배열의 선언과 생성

선언방법 선언 예
타입[] 변수이름; int[] score;
String[] name;
타입 변수이름[]; int score[];
String name[];

 

a.  배열의 생성

타입[] 변수이름;                         //  배열을 선언 (배열을 다루기 위한 참조변수 선언)
변수이름 = new 타입[길이];    //  배열을 생성 (실제 저장공간을 생성)

 

  📍  각 배열요소는 자동적으로 int의 기본값(default)인 0으로 초기화

 


 2)  배열의 길이와 인덱스

 

  🚀  생성된 배열의 각 저장공간을 '배열의 요소(element)'라고 하며, '배열이름[인덱스]'의 형식으로 배열의 요소에 접근한다.

  🚀  인덱스(index)는 배열의 요소마다 붙여진 일련번호

         ➡️  인덱스의 범위는 0부터 '배열길이 -1'까지

         ➡️  인덱스로 상수 대신 변수나 수식도 사용할 수 있다

​score[0] = 0 ;
score[1] = 10;
score[2] = 20;
score[3] = 30;
score[4] = 40;

​* [0],[1],[2],[3],..  : '인덱스'(위치를 알려주는 원리)
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(배열이름) 메서드 사용

          → 배열의 모든 요소를 '[첫번째 요소, 두번째 요소, ...]' 와 같은 형식의 문자열로 만들어서 반환

  🚀  아래 변수 iArr 값을 바로 출력하면 '타입 @ 주소' 형식으로 출력된다.

        참조변수는 heap 영역에, 지역변수&매개변수는 stack 영역에 저장 됨.

-> 그렇기 때문에 참조 변수는 주소로 출력됨.

int[] iArr = {100, 95, 80, 70, 60}; 

for(int i = 0; i < iArr.length; i++) {
    System.out.println(iArr[i] + ","); 
}

System.out.println();  
System.out.prinln(iArr); // [I@7344699f
System.out.prinln(Arrays.toString(iArr)); // [100, 95, 80, 70, 60]

 


  ✓  메모리 구조

 

☕ 그림으로 보는 자바 코드의 메모리 영역(스택 & 힙)

자바의 메모리 영역 이번 포스팅에선 자바(JAVA)를 사용하는 입장에서 알아야 할 메모리 구조 및 특징에 대해서 알아보려고 한다. 자바 프로그램이 실행되면 JVM(자바 가상 머신)은 OS로부터 메모

inpa.tistory.com

 


 

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();
    }
}
 
 실행 예)
        -----------------------------------------------------
        1.학생수 | 2.점수입력 | 3.점수리스트 | 4.분석 | 5.종료
        -----------------------------------------------------
        선택> 1
        학생수> 3
        [0, 0, 0]
        -----------------------------------------------------
        1.학생수 | 2.점수입력 | 3.점수리스트 | 4.분석 | 5.종료
        -----------------------------------------------------
        선택> 2
        scores[0]> 90
        scores[1]> 80
        scores[2]> 70
        -----------------------------------------------------
        1.학생수 | 2.점수입력 | 3.점수리스트 | 4.분석 | 5.종료
        -----------------------------------------------------
        선택> 3
        scores[0]: 90
        scores[1]: 80
        scores[2]: 70
        -----------------------------------------------------
        1.학생수 | 2.점수입력 | 3.점수리스트 | 4.분석 | 5.종료
        -----------------------------------------------------
        선택> 4
        최고 점수: 90
        평균 점수: 80.0
        -----------------------------------------------------
        1.학생수 | 2.점수입력 | 3.점수리스트 | 4.분석 | 5.종료
        -----------------------------------------------------
        선택> 5
        프로그램 종료

 

 
 
 

 

* 내용 참고 : 학원강의 및 자바의 정석 3rd

 

 


1.  while 문

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();
	}
}

 

 

 

 

 

[ 출처 ; 학원 강의 및 자바의 정석 책 ]


ERD

 

 

상품이미지 엔티티 구현


  • ItemImg 클래스 생성
@Entity
@Table(name = "item_img")
@Data
public class ItemImg extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "item_img_id")
    private Long id;

    @Column(name = "image_name", nullable = false)
    private String imgName;  // 이미지 파일명

    @Column(name = "original_name")
    private String oriImgName;  // 원본 이미지 파일명

    @Column(name = "image_url")
    private String imgUrl;  // 이미지 조회 경로

    @Column(name = "represent_img")
    private String repImgYn;  // 대표 이미지 여부

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    public void updateItemImg(String oriImgName, String imgName, String imgUrl) {

        this.oriImgName = oriImgName;
        this.imgName = imgName;
        this.imgUrl = imgUrl;
    }
}

 

  ✔️  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을 해야 함.
  • 특히 데이터끼리 서로 연관이 있으면 어떤 데이터가 변함에 따라 다른 데이터도 함께 체크를 해야하는 경우 빈번.

 

 

상품 등록 페이지 구현


  • ItemForm.html 생성 (코드가 길기 때문에 깃허브 소스 첨부)

https://github.com/SominY/Dora-Flower-Shop/blob/main/src/main/resources/templates/item/itemForm.html

 

Dora-Flower-Shop/src/main/resources/templates/item/itemForm.html at main · SominY/Dora-Flower-Shop

쇼핑몰 프로젝트 (개인 프로젝트). Contribute to SominY/Dora-Flower-Shop development by creating an account on GitHub.

github.com

 

 

* 앞에 memberForm 페이지 참고해서 부트스트랩 코드 작성함..! 아쉬운 점은 부트스트랩 파일 업로드 형식이 너무 제한적.. ㅠ 저런 디자인 말고 다른거 하고 싶은데 버전별로 다른듯..😣

 

        • 폼 전송할 때 enctype(인코딩 타입) 값으로 'multipart/form-data' 입력 - 웹 폼에서 파일을 업로드할 때 사용되는 인코딩 방식. 이 방식은 데이터와 파일을 서버로 전송할 때 각각의 데이터를 구분하고, 파일을 포함한 복합적인 데이터 전송을 가능하게 함

 

상품 등록 시

   

    - itemImgDTOList가 비어 있는 경우(새 상품 등록 시) 1~5개의 이미지 업로드 필드를 동적으로 생성.

    - itemFormDTO.id가 비어 있을 경우, Register 버튼을 표시

 

상품 수정 시

   

    - itemImgDTOList가 비어 있지 않은 경우(기존 상품 수정 시) 기존 이미지들을 수정할 수 있도록 이미지 업로드 필드를 생성.

    - itemImgIds는 숨겨진 필드로 기존 이미지 ID를 전달.

    - itemFormDTO.id가 존재할 경우, Update 버튼을 표시

 

#lists.isEmpty(...)

 

  • #lists는 Thymeleaf의 내장 객체로, 리스트와 관련된 유틸리티 메소드를 제공

  • isEmpty(...)#lists 객체의 메소드로, 리스트가 비어 있는지를 확인하는 기능

 

#strings.isEmpty(...)

 

  • 특정 문자열이 빈 문자열("")인지, 또는 null인지 여부를 검사

 

#numbers.sequence(start, end)

 

  • 지정된 시작 값과 끝 값 사이의 정수 시퀀스를 생성하여 반복할 수 있게 함

  • Thymeleaf 템플릿에서 주로 반복문을 생성할 때 유용

 

th:formaction

 

  • 폼의 제출 URL을 동적으로 설정

  • 등록과 수정 작업에 따라 서로 다른 URL로 폼을 제출할 수 있도록 함

 

 

자바스크립트 코드

⚡️ 외부로 파일 빼고 싶으면 부트스트랩 jquery 코드 html에 넣어줘야함!!!!!

더보기

1. $(".custom-file-input").on("change", function () { ... });

  • custom-file-input 클래스를 가진 HTML 요소에 change 이벤트 리스너를 추가. 사용자가 파일 선택 필드에서 파일을 선택할 때마다 이 함수가 실행.

 

2. var fileName = $(this).val().split("\\").pop();

  • 사용자가 선택한 파일의 이름을 가져옴

  • 작동 방식: $(this).val() - 현재 변경된 input 요소의 값을 가져옴. 이 값은 선택된 파일의 경로.

                     split("\\") - 경로를 역슬래시(\)를 기준으로 분리하여 파일 이름을 포함한 배열을 생성.

                     pop() - 배열의 마지막 요소, 즉 파일 이름을 가져옴.

 

3. var fileExt = fileName.substring(fileName.lastIndexOf(".") + 1);

  • 파일의 확장자를 추출

  • 작동 방식 : fileName.lastIndexOf(".") + 1 - 파일 이름에서 마지막 점(.)의 위치를 찾고, 이 위치 다음 문자부터 문자열의 끝까지를 가져옴.

                     substring(...) - 파일 확장자를 추출

 

4. fileExt = fileExt.toLowerCase();

  • 파일 확장자를 소문자로 변환하여 대소문자 구분 없이 파일 형식을 검사할 수 있도록 함

 

5. if (fileExt != "jpg" && fileExt != "jpeg" && fileExt != "gif" && fileExt != "png" && fileExt != "bmp") { ... }

  • 파일 확장자가 허용된 이미지 파일 형식인지 확인

  • 허용되지 않는 파일 형식인 경우 사용자에게 알림을 띄우고 함수 실행을 종료 (return)

 

6. $(this).siblings(".custom-file-label").html(fileName);

  • 선택한 파일의 이름을 .custom-file-label 클래스를 가진 형제 요소에 표시

  • 작동 방식 : $(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 파일을 찾아서 반환.

 

https://jake-seo-dev.tistory.com/605

 

스프링 WebMvcConfigurer 인터페이스란?

WebMvcConfigurer 란? 스프링 프레임워크에서 제공하는 인터페이스이다. 보일러플레이트 코드 없이 요구사항에 맞게 프레임워크를 조정할 수 있게 해준다. 특정한 스프링 클래스를 구현하거나 상속

jake-seo-dev.tistory.com

 

 

 

파일을 처리하는 Service 구현


public String uploadFile(String uploadPath, String originalFileName, 
    byte[] fileData) throws Exception {
    
    // 서로 다른 개체들을 구별하기 위해서 이름을 부여할 때 사용
    UUID uuid = UUID.randomUUID();
    
    String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
    String savedFileName = uuid + extension;
    String fileUploadFullUrl = uploadPath + "/" + savedFileName;

    FileOutputStream fos = new FileOutputStream(fileUploadFullUrl);
    fos.write(fileData);
    fos.close();

    return savedFileName;
}

 

 

UUID 생성 : UUID.randomUUID()를 사용하여 파일 이름 충돌을 방지. UUID는 파일 이름의 일부분으로 사용되며, 파일의 유일성을 보장.

파일 확장자 추출 : originalFileName에서 파일 확장자를 추출. 예를 들어, .jpg, .png와 같은 확장자를 추출.

파일 이름 생성 : UUID와 파일 확장자를 결합하여 저장할 파일 이름을 생성.

파일 경로 구성 : uploadPathsavedFileName을 결합하여 파일의 전체 경로를 생성.

파일 저장

    -  FileOutputStream 객체를 생성하여 지정된 파일 경로 (fileUploadFullUrl)로 데이터를 쓸 수 있는 출력 스트림을 준비

    -  바이트 배열 fileData를 파일에 기록. fileData는 파일의 실제 데이터이며, 이 데이터가 FileOutputStream을 통해 파일에 기록

    -  스트림을 닫아 파일 출력을 완료하고 자원을 해제

예외 처리 : throws Exception을 선언하여 예외 발생 시 호출자에게 전달. 예를 들어, 파일 저장 중 IO 오류가 발생할 수 있음.

 

UUID는 'Universally Unique Identifier'의 약자로 128-bit의 고유 식별자. 다른 고유 ID 생성 방법과 다르게 UUID는 중앙 시스템에 등록하고 발급하는 과정이 없어서 상대적으로 더 빠르고 간단하게 만들 수 있다는 장점이 있다.

 

https://docs.tosspayments.com/resources/glossary/uuid

 

UUID(Universally Unique Identifier) | 토스페이먼츠 개발자센터

UUID는 128-bit의 고유 식별자에요. UUID는 중앙 시스템에 등록하고 발급하는 과정이 없어서 상대적으로 빠르고 간단하게 만들 수 있어요.

docs.tosspayments.com

 

 

    public void deleteFile(String filePath) throws Exception{

        File deleteFile = new File(filePath);

        if(deleteFile.exists()) {
            deleteFile.delete();
            log.info("파일을 삭제하였습니다.");
        } else {
            log.info("파일이 존재하지 않습니다.");
        }

    }

 

File 클래스 : java.io.File 클래스는 파일 및 디렉토리에 대한 경로를 나타내며, 파일 시스템에서 파일이나 디렉토리에 접근하고 조작하는 기능을 제공

 

 

ItemImgRepository 구현


public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {

}

 

 

 

ItemImgService, ItemImgServiceImpl 구현


public interface ItemImgService {

    void saveItemImg(ItemImg itemImg, MultipartFile multipartFile)
            throws Exception;
}
@Service
@Transactional
@RequiredArgsConstructor
public class ItemImgServiceImpl implements ItemImgService {

    @Value("${itemImgLocation}")
    private String itemImgLocation;

    private final ItemImgRepository itemImgRepository;

    private final FileService fileService;

    @Override
    public void saveItemImg(ItemImg itemImg, MultipartFile itemImgFile) throws Exception {

        String oriImgName = itemImgFile.getOriginalFilename();
        String imgName = "";
        String imgUrl = "";

        // 파일 업로드
        if(!StringUtils.isEmpty(oriImgName)) {
            imgName = fileService.uploadFile(itemImgLocation, oriImgName,
                    itemImgFile.getBytes());
            imgUrl = "/images/item/" + imgName;
        }

        // 상품 이미지 정보 저장
        itemImg.updateItemImg(oriImgName, imgName, imgUrl);
        itemImgRepository.save(itemImg);

    }
}

 

 

더보기

MultipartFile 

 

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 메소드 추가
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {

    List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
}
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemServiceImplTest {

    @Autowired
    ItemService itemService;

    @Autowired
    ItemRepository itemRepository;

    @Autowired
    ItemImgRepository itemImgRepository;

    List<MultipartFile> createMultipartFiles() throws Exception {

        List<MultipartFile> multipartFileList = new ArrayList<>();

        for (int i=0; i<5; i++) {

            String path = "/Users/Desktop/e-commerce project/item";
            String imageName = "image" + i + ".jpg";

            MockMultipartFile multipartFile =
                    new MockMultipartFile(path, imageName, "image/jpg", new byte[]{1,2,3,4});
            multipartFileList.add(multipartFile);
        }

        return multipartFileList;
    }

    @Test
    @DisplayName("상품 등록 테스트")
    @WithMockUser(username = "admin", roles = "ADMIN")
    void saveItem() throws Exception {

        ItemFormDTO itemFormDTO = ItemFormDTO.builder()
                .itemNm("테스트상품")
                .itemSellStatus(ItemSellStatus.SELL)
                .itemDetail("테스트 상품입니다.")
                .price(1000)
                .stockNum(100)
                .build();

        List<MultipartFile> multipartFileList = createMultipartFiles();
        Long itemId = itemService.saveItem(itemFormDTO, multipartFileList);

        List<ItemImg> itemImgList =
                itemImgRepository.findByItemIdOrderByIdAsc(itemId);
        Item item = itemRepository.findById(itemId)
                .orElseThrow(EntityNotFoundException::new);

        assertEquals(itemFormDTO.getItemNm(), item.getItemNm());
        assertEquals(itemFormDTO.getItemSellStatus(), item.getItemSellStatus());
        assertEquals(itemFormDTO.getItemDetail(), item.getItemDetail());
        assertEquals(itemFormDTO.getPrice(), item.getPrice());
        assertEquals(itemFormDTO.getStockNum(), item.getStockNum());
        assertEquals(multipartFileList.get(0).getOriginalFilename(),
                itemImgList.get(0).getOriImgName());

    }

}

 

  • MockMultipartFile : Spring의 MockMultipartFile 클래스는 테스트 중 파일 업로드를 시뮬레이션하는 데 사용된다. 실제 파일이 아닌 가상의 파일을 생성할 수 있음. path, imageName, contentType, content를 매개변수로 받아 파일의 메타데이터와 내용을 설정함.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/mock/web/MockMultipartFile.html

 

MockMultipartFile (Spring Framework 6.1.11 API)

getContentType Return the content type of the file. Specified by: getContentType in interface MultipartFile Returns: the content type, or null if not defined (or no file has been chosen in the multipart form)

docs.spring.io

 

 

 

 

 

 

내용 참고 : 책 "스프링 부트 쇼핑몰 프로젝트 with JPA"


개발환경

 

  1. 운영체제 : Mac OS
  2. 통합개발환경(IDE) : IntelliJ Ultimate
  3. JDK version : JDK 22
  4. Spring Boot version : 3.3.2
  5. Database : MySQL
  6. Build Tool : Maven
  7. Packaging : Jar

 

Dependencies
  • Spring Web
  • Spring Data JPA
  • MySQL Driver
  • H2 Database
  • Thymeleaf
  • Lombok
  • Spring Security
  • Spring Boot DevTools
  • querydsl
  • thymeleaf-layout-dialect
  • validation
  • thymeleaf-extras-springsecurity6

 

패키지 설계

 

스프링 시큐리티 설정


  • config 패키지에 securityconfig 클래스 생성
@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 또는 비상 상황에서는 비활성화할 수 있다.

 

https://wikidocs.net/162150

 

3-05 스프링 시큐리티란?

* `[완성 소스]` : [https://github.com/pahkey/sbb3/tree/v3.05](https://github.com/pahkey/sbb3/tree/v3.05…

wikidocs.net

 

📍 내가 노션에 정리한 내용!

https://www.notion.so/5-Spring-Boot-Security-b95a14467d6f4a87937a66b8d217c2d2?pvs=4

https://www.notion.so/8-Spring-MVC-Security-3fc33d56e52e4d809dac4b7fbdc0f025?pvs=4

 

 

.requestMatchers("/**").permitAll() - 일단 스프링 시큐리티 적용안하고 바로 화면 보이도록 설정...

 

 

엔티티, DTO 코드 설정


 

1. 일반 유저 / 관리자 역할 코드

  •  com.doraflower 패키지 아래 'constant' 키지 생성 - Role.java 생성
  •  enum(열거형)

 

https://velog.io/@mooh2jj/Java-Enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

 

[Java] Enum을 사용하는 이유와 활용법

Java enum은 제한된 값 목록을 갖는 타입입니다. enum은 다음과 같은 이점을 갖습니다.enum은 컴파일 타임에 타입 안정성을 보장합니다. 특정 범위의 값만 사용 가능하므로 컴파일 오류나 런타임 예

velog.io

 

public enum Role {
    USER, ADMIN
}

 

 

2. MemberFormDTO 클래스 코드 - 회원가입 화면으로부터 넘어오는 정보를 담는다.

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberFormDTO {

    @NotBlank(message = "Name is required")
    private String name;

    @NotBlank(message = "Email is required")
    @Pattern(regexp = "^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
            message = "Please enter a valid email address.")
    private String email;

    @NotBlank(message = "Password is required")
    @Length(min=8, max=16, message = "Enter a password between 8 and 16 characters, including numbers.")
    private String password;

    @NotBlank(message = "Address is required")
    private String address;
}

 

2024.07.25 - [Spring & Spring Boot] - @Lombok 및 Entity 관련 어노테이션

 

  • 회원가입 폼에서 받을 내용 - 이름 | 이메일 주소 (로그인 시 확인) | 비밀번호 | 주소
  • 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 메소드를 이용해서 저장하려고 요청했던 값과 실제 저장된 데이터를 비교함.
  • assertThrows 메소드 이용하면 예외 처리 테스트가 가능.

https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html

 

Assertions (JUnit 5.0.1 API)

Asserts that all supplied executables do not throw exceptions. If any supplied Executable throws an exception (i.e., a Throwable or any subclass thereof), all remaining executables will still be executed, and all exceptions will be aggregated and reported

junit.org

 

https://velog.io/@roycewon/JUnit-Assert-Methods1

 

JUnit - Assert Methods(1)

JUnit 5 모듈인 Jupiter는 JUnit4에 있는 Assertion method를 포함하여 여러 메소드를 제공한다. Java8에서 추가된 람다와 함께 사용하면 좋다.Assert method는 org.junit.jupiter.api.Assertions 라는 클래

velog.io

 

 

 

Controller 코드 구현


  • controller 패키지 아래 MemberController 생성
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/new")
    public String memberForm(Model model) {

        model.addAttribute("memberFormDTO", new MemberFormDTO());
        return "member/memberForm";
    }
    
    @PostMapping("/new")
    public String memberForm(@Valid MemberFormDTO memberFormDTO,
                             BindingResult bindingResult,
                             Model model) {

        // 유효성 오류시 실행할 메서드
        if (bindingResult.hasErrors()) {
            System.out.println("Binding results: " + bindingResult.toString());
            return "member/memberForm";
        }

        // 이메일 중복 시 예외처리
        try {
            Member member = Member.createMember(memberFormDTO, passwordEncoder);
            memberService.saveMember(member);
        } catch (IllegalStateException e){
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }

        // 성공 시 리다이렉트
        return "redirect:/";
    }
}

 

  • ModelmemberFormDTO라는 이름으로 새로운 MemberFormDTO 객체를 추가. 뷰에서 이 객체를 사용하여 폼을 렌더링할 수 있다.
  • localhost:8080/members/new 접속하면 member 패키지 아래에 있는 memberForm.html 화면이 출력됨.
  • @PostMapping은 form 을 제출했을 때 실행되는 코드~ 페이지 구현하고 작성해도 됨!

 

 

회원가입 페이지 구현


  • templates 패키지 아래 member 패키지 생성 - memberForm.html 생성
  • Thymeleaf 템플릿 사용해서 작성함.
  • fragments 패키지 아래에 header/footer html 만들어서 layout01.html에 fragment로 설정하고 content영역만 따로 페이지 구현해서 연동되도록 만들었습니닷,

2024.05.09 - [Spring & Spring Boot] - [Spring Boot] Thymeleaf

https://github.com/SominY/Dora-Flower-Shop/blob/main/src/main/resources/templates/member/memberForm.html(소스코드)

 

Dora-Flower-Shop/src/main/resources/templates/member/memberForm.html at main · SominY/Dora-Flower-Shop

쇼핑몰 프로젝트 (개인 프로젝트). Contribute to SominY/Dora-Flower-Shop development by creating an account on GitHub.

github.com

 

 

 

 

 

 

 

내용 참고: 책 '스프링 부트 쇼핑몰 프로젝트 with JPA'


JpaRepositoy 지원 메소드

메소드 기능
<S extends T> save(S entity) 엔티티 저장 및 수정
void delete(T entity) 엔티티 삭제
count() 엔티티 총 개수 반환
Iterable<T> findAll() 모든 엔티티 조히

 

📍 연관 페이지

https://www.notion.so/4-Spring-Boot-Rest-CRUD-b666e9cbc99746048343b5ac0b464871?pvs=4

2024.05.25 - [Spring & Spring Boot] - [Spring Boot] Spring Data JPA (1)

 

[Spring Boot] Spring Data JPA (1)

1.  Spring Data JPAJDBC에서 작업하다가 MyBatis를 배울 경우에는 SQL 작성법도 거의 동일하고, 코드의 간략화가 목표여서 러닝커브가 높지 않지만JPA의 경우에는 JDBC, MyBatis와는 사용방법이 상당한

allriver.tistory.com

 


Query method & JPQL snippet

Keyword Sample JPQL snippet
And findByLastnameAndFirstname ... where x.lastname =  ?1 and x.firstname = ?2
Or findByLastnameOrFirstname ... where x.lastname =  ?1 or x.firstname = ?2
Is, Equals findByFirstname
findByFirstnameIs
findByFirstnameEquals
... where x.firstname = ?1
Between findByStartDateBetween ... where x.startDate between ?1 and ?2
LessThan findByAgeLessThan ... where x.age < ?1
LessThanEqual findByAgeLessThanEqual ... where x.age <= ?1
GreaterThan findByAgeGreaterThan ... where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual ... where x.age >= ?1
After findByStartDateAfter ... where x.startDate > ?1
Before findByStartDateBefore ... where x.startDate < ?1
IsNull, Null
IsNotNull
findByAge(Is)Null ... where x.age is null
NotNull findByAge(Is)NotNull ... where x.age not null
Like findByAgeFirstnameLike ... where x.firstname like ?1
NotLike findByAgeFirstnameNotLike ... where x.firstname not like ?1
StartingWith findByFirstnameStartingWith ... where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith ... where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining ... where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc ... where x.age ?1 order by x.lastname desc
Not findByLastnameNot ... where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) ... where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) ... where x.age not in ?1
True findByActiveTrue() ... where x.active = true
False findByActiveFalse() ... where x.active = false
IgnoreCase findBYFirstnameIgnoreCase ... where UPPER(x.firstname) = UPPER(?1)

 

 

 


 

javax.validation 어노테이션 예시

어노테이션 설명
@NotEmpty NULL 체크 및 문자열의 경우 길이 0인지 검사
@NotBlank NULL 체크 및 문자열의 경우 길이 0 및 빈 문자열(" ") 검사
@Length(min=, max=) 최소, 최대 길이 검사
@Email 이메일 형식인지 검사
@Max(숫자) 지정한 값보다 작은지 검사
@Min(숫자) 지정한 값보다 큰지 검사
@Null 값이 NULL인지 검사
@NotNull 값이 NULL이 아닌지 검사

 

 

 

 

 

 

* 내용 참고 : 책 '스프링부트 쇼핑몰 프로젝트 with JPA'

+ Recent posts