1.  객체 타입 확인

boolean result = 객체 instance of 타입;

 

🚀  매개변수의 다형성에서 실제로 어떤 객체가 매개값으로 제공되었는지 확인할 때 사용

🚀  꼭 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인하고자 할 때 instance of 연산자를 사용할 수 있다

  • 좌항에는 객체가 오고 우항에는 타입이 위치
  • 좌항의 객체가 우항의 타입이 일치하면 true를 산출하고 그렇지 않으면 false를 산출
if(parent instance of Child child) {
    // child 변수 사용
}

 

사용 예제

 

public class Person {
	//필드 선언
	public String name;

	//생성자 선언
	public Person(String name) {
		this.name = name;
	}

	//메소드 선언
	public void walk() {
		System.out.println("걷습니다.");
	}
}
public class Student extends Person {
	//필드 선언
	public int studentNo;

	//생성자 선언
	public Student(String name, int studentNo) {
		super(name);
		this.studentNo = studentNo;
	}

	//메소드 선언
	public void study() {
		System.out.println("공부를 합니다.");
	}
}

 

public class InstanceofExample {

	//main() 메소드에서 바로 호출하기 위해 정적 메소드 선언
	public static void personInfo(Person person) {
		System.out.println("name: " + person.name);
		person.walk();

		//person이 참조하는 객체가 Student 타입인지 확인
		/*if (person instanceof Student) {
        
 			//Student 객체일 경우 강제 타입 변환
 			Student student = (Student) person;
            
 			//Student 객체만 가지고 있는 필드 및 메소드 사용
 			System.out.println("studentNo: " + student.studentNo);
 			student.study();
            
 		}*/

		// person이 참조하는 객체가 Student 타입일 경우
		// student 변수에 대입(타입 변환 발생)- java 12~
		if(person instanceof Student student) {
			System.out.println("studentNo: " + student.studentNo);
			student.study();
		}
	}

	public static void main(String[] args) {
		//Person 객체를 매개값으로 제공하고 personInfo() 메소드 호출
		Person p1 = new Person("홍길동");
		personInfo(p1);
		
		System.out.println();

		//Student 객체를 매개값으로 제공하고 personInfo() 메소드 호출
		Person p2 = new Student("김길동", 10);
		personInfo(p2);
	}
    
}

 

 

 

* 내용 참고 - 책 '이것이 자바다'

'Programming Language > JAVA' 카테고리의 다른 글

[Java] 다형성  (0) 2024.08.24
[Java] 타입 변환  (0) 2024.08.24
[Java] final 클래스와 final 메소드 · protected 접근 제한자  (0) 2024.08.23
[Java] 오버라이딩 Overriding  (0) 2024.08.11
[Java] 상속 Inheritance  (0) 2024.08.11


 

1.  다형성  polymorphism

🚀  사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말함

🚀  객체는 부품과 같아서 프로그램을 구성하는 객체를 바꾸면 프로그램의 실행 성능이 다르게 나올 수 있다.

 

  • 객체 사용 방법이 동일하다는 것은 동일한 메소드를 가지고 있다는 뜻
  • 한국 타이어와 금호 타이어는 모두 타이어를 상속하고 있으므로 부모의 메소드를 동일하게 가지고 있다고 말할 수 있다.
  • 타이어 메소드 호출 시 오버라이딩 된 메소드가 호출되는데, 오버라이딩 된 내용은 두 타이어가 다르기 때문에 실행 결과가 다르게 나옴
  • 이것을 '다형성' 이라고 하는데 다형성을 구현하기 위해서는 자동 타입 변환과 메소드 재정의가 필요하다.


1)  필드 다형성

  👾  필드 타입은 동일하지만(사용 방법은 동일하지만) 대입되는 객체가 달라져서 실행 결과가 다양하게 나올 수 있는 것을 말한다.

 

public class Car {
	//필드 선언
	public Tire tire;

	//메소드 선언
	public void run() {
		//tire 필드에 대입된 객체의 roll() 메소드 호출
		tire.roll();
	}
}
public class Tire {
	//메소드 선언
	public void roll() {
		System.out.println("회전합니다.");
	}
}

 

public class HankookTire extends Tire {
	//메소드 재정의(오버라이딩)
	@Override
	public void roll() {
		System.out.println("한국 타이어가 회전합니다.");
	}
}
public class KumhoTire extends Tire {
	//메소드 재정의(오버라이딩)
	@Override
	public void roll() {
		System.out.println("금호 타이어가 회전합니다.");
	}
}

 

public class CarExample {
	public static void main(String[] args) {
		//Car 객체 생성
		Car myCar = new Car();

		//Tire 객체 장착
		myCar.tire = new Tire();
		myCar.run();

		//HankookTire 객체 장착
		myCar.tire = new HankookTire();
		myCar.run();

		//KumhoTire 객체 장착
		myCar.tire = new KumhoTire();
		myCar.run();
	}
}

 

  • Car 클래스의 run() 메소드는 tire 필드에 대입된 객체의 roll() 메소드를 호출한다.
  • 어떤 타이어를 장착했는지에 따라 roll() 메소드의 실행 결과는 달라지게 된다. 이것이 바로 '필드의 다형성'이라고 함

 

2)  매개변수 다형성

  👾  다형성은 필드보다는 메소드를 호출할 때 많이 발생함

  👾  메소드가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만 자식 객체를 제공할 수도 있다.

 

public class Driver {
    public void drive(Vehicle vehicle) {
      vehicle.run();
    }
}
  • drive() 메소드는 매개값으로 전달받은 vehicle의 run() 메소드를 호출함
Driver driver = new Driver();
Vehicle vehicle = new Vehicle();
driver.drive(vehicle);

 

  • 일반적으로 drive() 메소드를 호출한다면 위와 같이 Vehicle의 객체를 제공
  • but, 자동 타입 변환으로 인해 매개값으로 Vehicle의 자식 객체도 제공할 수 있음!

  ⚡️  drive() 메소드는 매개변수 vehicle이 참조하는 객체의 run() 메소드를 호출하는데, 자식 객체가 run() 메소드를 재정의하고 있다면 재정의된 run() 메소드가 호출된다.

      ☑️  어떤 자식 객체가 제공하느냐에 따라 drive() 실행 결과는 달라진다. 이것이 '매개변수의 다형성'

 

public class Vehicle {
	//메소드 선언
	public void run() {
		System.out.println("차량이 달립니다.");
	}
}
public class Bus extends Vehicle {
	//메소드 재정의(오버라이딩)
	@Override
	public void run() {
		System.out.println("버스가 달립니다.");
	}
}
public class Taxi extends Vehicle {
	//메소드 재정의(오버라이딩)
	@Override
	public void run() {
		System.out.println("택시가 달립니다.");
	}
}
public class Driver {
	//메소드 선언(클래스 타입의 매개변수를 가지고 있음)
	public void drive(Vehicle vehicle) {
		vehicle.run();
	}
}
public class DriverExample {
	public static void main(String[] args) {
		//Driver 객체 생성
		Driver driver = new Driver();

		//매개값으로 Bus 객체를 제공하고 driver() 메소드 호출
		Bus bus = new Bus();
		driver.drive(bus);

		//매개값으로 Taxi 객체를 제공하고 driver() 메소드 호출
		Taxi taxi = new Taxi();
		driver.drive(taxi);
	}
}

 

 

응용 예제
public class Product {
    int price; // 제품의 가격
    int bonusPoint; // 제품구매 시 제공하는 보너스 점수

    Product(int price){
        this.price = price;
        this.bonusPoint = (int)(price / 10.0); // 보너스점수는 제품가격의 10%
    }
}

 

public class Tv extends Product{
    Tv () {
        // 부모클래스의 생성자 Product(int price)를 호출한다.
        super(100); // Tv의 가격을 100만원으로 한다.
    }

    @Override
    public String toString() {
        return "Tv";
    }
}
public class Audio extends Product{
    Audio() {
        super(50);
    }

    @Override
    public String toString() {
        return "Audio";
    }
}
public class Computer extends Product{
    Computer () {
        super(200);
    }

    @Override
    public String toString() {
        return "Computer";
    }
}

 

1. Object 클래스 상속:
    -  모든 Java 클래스는 자동으로 Object 클래스를 상속받는다. Object 클래스는 모든 클래스의 최상위 부모 클래스이고, 이 클래스에서 제공하는 여러 메서드들이 모든 클래스에서 사용할 수 있게 된다. 그 중 하나가 toString() 메서드

2. toString() 메서드:
    -  Object 클래스에는 이미 toString() 메서드가 정의되어 있기 때문에, 어떤 클래스든 toString() 메서드를 오버라이딩할 수 있다. 따라서, Product 클래스에 따로 toString() 메서드를 정의하지 않더라도, Tv 클래스에서 이 메서드를 오버라이딩하는 것이 가능

 

public class Buyer { // 고객, 물건을 사는 사람
        int money = 1000; // 소유금액
        int bonusPoint = 0; // 보너스점수

        /* Product[] products = new Product[10]; // 구입한 제품을 저장하기 위한 배열
        int i = 0; // Product 배열에 사용될 카운터 */
        
        ArrayList<Product> products = new ArrayList<>();

        void buy (Product product){ // 부모클래스 타입으로 매개변수 받음.
            // 부모 클래스의 필드 사용. price, bonusPoint
            if (money < product.price) {
                System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
                return;
            }
            money -= product.price;  // 가진 돈에서 구입한 제품의 가격을 뺀다.
            bonusPoint += product.bonusPoint; // 제품의 보너스 점수를 추가한다.
            products.add(product); // 구입한 제품을 ArrayList에 저장한다.
            System.out.println(product + " 을/를 구입하셨습니다.");
        }
        
        void summary () { // 구매한 물품에 대한 정보를 요약해서 보여 준다.
            int sum = 0; // 구입한 물품의 가격합계
            String itemList = ""; // 구입한 물품목록

            if(products.isEmpty()) { // ArrayList가 비어있는지 확인한다.
                System.out.println("구입하신 제품이 없습니다.");
                return;
            }
            // ArrayList의 i번째에 있는 객체를 얻어 온다.
            for(int i =0; i < products.size(); i++) {
                Product product = products.get(i);
                sum += product.price;
                itemList += (i==0) ? "" + product : ", " + product;
            }


          /*  // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다.
            // 1) for 이용
            for (int i = 0; i < products.length; i++) {
                if (products[i] == null)
                    break;
                sum += products[i].price;
                itemList += products[i] + ", ";
            }
            // 2) foreach 사용
            // for(각 요소의 타입과 요소를 담을 변수 : 배열 또는 컬렉션)
            for (Product product : products) {
                if (product == null)
                    break;
                sum += product.price;
                itemList += product + ", ";
            }

            // 3) 반복을 줄이기 위해 인스턴스 변수 사용
            for (int i = 0; i < this.i; i++) {
                sum += products[i].price;
                itemList += products[i] + ", ";
            } */

            System.out.println("구입하신 물품의 총금액은 " + sum + "만원입니다.");
            System.out.println("구입하신 제품은 " + itemList + "입니다.");
        }

        void refund(Product product) { // 구입한 제품을 환불한다.
            if (products.remove(product)) { // 제품을 ArrayList에서 제거한다.
                money += product.price;
                bonusPoint -= product.bonusPoint;
                System.out.println(product + "을/를 반품하셨습니다.");
            } else { // 제거에 실패한 경우
                System.out.println("구입하신 제품 중 해당 제품이 없습니다.");
            }
        }
        
}
public class Test {
    /*
    코딩 순서 : Product -> Tv -> Computer -> Buyer -> Test
     */
    public static void main(String[] args) {
        Buyer b = new Buyer();
        Tv tv = new Tv();
        Computer com = new Computer();
        Audio audio = new Audio();

        b.buy(tv); // Tv 을/를 구입하셨습니다.
        b.buy(com); // Computer 을/를 구입하셨습니다.
        b.buy(audio); // Audio 을/를 구입하셨습니다.
        b.summary(); // 구입하신 물품의 총금액은 350만원입니다. 구입하신 제품은 Tv, Computer, Audio입니다.
        System.out.println(); 
        b.refund(com); // Computer을/를 반품하셨습니다.
        b.summary(); // 구입하신 물품의 총금액은 150만원입니다. 구입하신 제품은 Tv, Audio입니다.
    }
}

 

 

 

 

* 내용 참고 - 책 '이것이 자바다' 및 학원 강의 자료


1.  타입 변환

⚒️  타입을 다른 타입으로 변환하는 것을 말함

⚒️  클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생

 

 

1)  자동 타입 변환  Promotion

부모타입 변수 = 자식타입객체;

 

  📍 자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급될 수 있다.

Cat cat = new Cat();
Animal animal = cat;

  📍  cat과 animal 변수는 타입만 다를 뿐, 동일한 Cat 객체를 참조

cat == animal  // true

class A {
}

class B extends A {
}

class C extends A {
}

class D extends B {
}

class E extends C {
}

public class PromotionExample {
	public static void main(String[] args) {
		B b = new B();
		C c = new C();
		D d = new D();
		E e = new E();

		A a1 = b;
		A a2 = c;
		A a3 = d;
		A a4 = e;
		
		B b1 = d;
		C c1 = e;
		
		// B b3 = e;
		// C c2 = d;
	}
}

 

  • 부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능
  • 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정
  • 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드 대신 오버라이딩된 메소드가 호출됨


 

2)  강제 타입 변환  Casting

자식타입 변수 = (자식타입) 부모타입객체;
Parent parent = new Child();   // 자동 타입 변환
Child child = (Child) parent;  // 강제 타입 변환

 

⚒️  부모 타입은 자식 타입으로 자동 변환되지 않는다. 대신 캐스팅 연산자로 강제 타입 변환을 할 수 있다.

⚒️  자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용 가능  

       ➡️  만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식타입으로 변환해야 함

 

 

 

 

 

 

* 내용 참고 - 책 '이것이 자바다'

 


 

1.  final 클래스와 final 메소드

1)  final 클래스

public final class 클래스 { ... }

 

🚀  클래스를 선언할 때 final 키워드를 class 앞에 붙이면 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 됨

 

 

2)  final 메소드 

public final 리턴타입 메소드( 매개변수, ...) { ... }

 

🚀  메소드를 선언할 때 final 키워드를 붙이면 이 메소드는 최종적인 메소드이므로 오버라이딩할 수 없는 메소드가 됨

 

 


2.  protected 접근 제한자

 

🚀  같은 패키지에서는 default처럼 접근이 가능하나, 다른 패키지에서는 자식 클래스만 접근을 허용

package package1;

public class A {
	//필드 선언
	protected String field;

	//생성자 선언
	protected A() {
	}

	//메소드 선언
	protected void method() {
	}
}

 

같은 패키지
package package1;

public class B {
	//메소드 선언
	public void method() {
		A a = new A();		//o
		a.field = "value"; 	//o
		a.method(); 			//o
	}
}

 

다른 패키지
package package2;

import package1.A;

public class C {
	//메소드 선언
	public void method() {
		//A a = new A();		//x
		//a.field = "value"; 		//x
		//a.method(); 			//x
	}
}

 

다른 패키지 & 자식 클래스
package package2;

import package1.A;

public class D extends A {
	//생성자 선언
	public D() {
		//A() 생성자 호출
		super();				//o
	}
	
	//메소드 선언
	public void method1() {
		//A 필드값 변경
		this.field = "value"; 	//o
		//A 메소드 호출
		this.method(); 			//o
	}
	
	//메소드 선언
	public void method2() {
		//A a = new A();		//x
		//a.field = "value"; 	//x
		//a.method(); 			//x
	}	
}

 

  ⚡️  new 연산자를 사용해서 생성자를 직접 호출할 수는 없고, 자식 생성자에서 super()로 A 생성자를 호출할 수 있다.

 

 

 

 

 

* 내용 참고 : 책 '이것이 자바다'

'Programming Language > JAVA' 카테고리의 다른 글

[Java] 다형성  (0) 2024.08.24
[Java] 타입 변환  (0) 2024.08.24
[Java] 오버라이딩 Overriding  (0) 2024.08.11
[Java] 상속 Inheritance  (0) 2024.08.11
[Java] Getter와 Setter · 싱글톤 패턴  (0) 2024.08.11


 

상품 조회 조건
  • 상품 등록일
  • 상품 판매 상태 [SELL/SOLD OUT]
  • 상품명 또는 상품 등록자 아이디

 

ItemSearchDTO 클래스 생성


  • QDomain 클래스 생성 후 ItemSearchDTO 클래스 생성
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ItemSearchDTO {

    // 상품등록일 
    private String searchDateType;

    // 판매상태
    private ItemSellStatus searchSellStatus;

    // 삼품 조회 유형
    private String searchBy;

    // 조회할 검색어 저장할 변수
    private String searchQuery;

}

 

Querydsl과 Spring Data Jpa를 함께 사용하기 위해서는 '사용자 정의 리파지토리'를 정의해야 함
  1. 사용자 정의 인터페이스 작성
  2. 사용자 정의 인터페이스 구현
  3. Spring Data Jpa 리포지토리에서 사용자 정의 인터페이스 상속
public interface ItemRepositoryCustom {

    Page<Item> getAdminItemPage(ItemSearchDTO itemSearchDTO, Pageable pageable);

}
  • 상품 조회 조건을 담고 있는 itemSearchDTO 객체와 페이징 정보를 담고 있는 pageable 객체를 파라미터로 받는 getAdminItemPage 메소드를 정의. 

 

public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{

    private JPAQueryFactory queryFactory;

    public ItemRepositoryCustomImpl(EntityManager em) {
        this.queryFactory = new JPAQueryFactory(em);
    }
    • 동적으로 쿼리 생성하기 위해 JPAQueryFactory 클래스 사용
    • JPAQueryFactory의 생성자로 EntityManager 객체를 넣어줌
    • EntityManager를 생성자에 주입하는 것은 JPAQueryFactory의 인스턴스를 생성하고 QueryDSL을 통해 데이터베이스 쿼리를 수행하기 위한 기본적인 설정 과정
    private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) {
        return searchSellStatus ==
                null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
    }

    private BooleanExpression regDtsAfter(String searchDateType) {
    // 해당 시간 이후로 등록된 상품만 조회
    
        LocalDateTime dateTime = LocalDateTime.now();

        if(StringUtils.equals("all", searchDateType) || searchDateType == null) {
            return null;
        } else if (StringUtils.equals("1d", searchDateType)) {
            dateTime = dateTime.minusDays(1);
        } else if (StringUtils.equals("1w", searchDateType)) {
            dateTime = dateTime.minusWeeks(1);
        } else if (StringUtils.equals("1m", searchDateType)) {
            dateTime = dateTime.minusMonths(1);
        } else if (StringUtils.equals("6m", searchDateType)) {
            dateTime = dateTime.minusMonths(6);
        }

        return QItem.item.regDate.after(dateTime);
    }

    private BooleanExpression searchByLike(String searchBy, String searchQuery) {
    // 상품명에 검색어를 포함하고 있는 상품 또는 상품 생성자 아이디에 검색어 포함하고 있는 상품 조회

        if(StringUtils.equals("itemNm", searchBy)) {
            return QItem.item.itemNm.like("%" + searchQuery + "%");
        } else if(StringUtils.equals("createdBy", searchBy)) {
            return QItem.item.createdBy.like("%" + searchQuery + "%");
        }

        return null;
    }
    • BooleanExpression은 QueryDSL에서 쿼리의 조건을 표현하는 데 사용되는 클래스
    • 일반적으로 SQL에서 WHERE 절에 해당하는 부분을 Java 코드에서 타입 안전하게 작성할 수 있게 해줌https://cs-ssupport.tistory.com/518
 

[QueryDSL] 동적 쿼리 (BooleanExpression)

QueryDSL은 SQL자체를 자바 코드로 작성하기 때문에 TypeSafe하고 컴파일 시점에 오류를 발견할 수 있다는 장점이 존재한다. 더불어서 QueryDSL의 가장 큰 장점 중 하나는 "동적 쿼리 생성의 편리함"이다

cs-ssupport.tistory.com

 

    @Override
    public Page<Item> getAdminItemPage(ItemSearchDTO itemSearchDTO, Pageable pageable) {

        // 조건에 따라 아이템 리스트를 조회
        List<Item> content = queryFactory
                .selectFrom(QItem.item)
                .where(regDtsAfter(itemSearchDTO.getSearchDateType()),
                        searchSellStatusEq(itemSearchDTO.getSearchSellStatus()),
                        searchByLike(itemSearchDTO.getSearchBy(),
                                itemSearchDTO.getSearchQuery()))
                .orderBy(QItem.item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();  // 쿼리를 실행하여 결과 리스트를 반환

        // 전체 레코드 수를 계산
        Long result = queryFactory.select(Wildcard.count).from(QItem.item)
                .where(regDtsAfter(itemSearchDTO.getSearchDateType()),
                        searchSellStatusEq(itemSearchDTO.getSearchSellStatus()),
                        searchByLike(itemSearchDTO.getSearchBy(), itemSearchDTO.getSearchQuery()))
                .fetchOne(); // 쿼리를 실행하여 단일 결과(전체 레코드 수)를 반환

        // NullPointerException을 방지
        long total = (result != null) ? result : 0L;

        return new PageImpl<>(content, pageable, total);
    }
  • queryFactory를 이용해서 쿼리를 생성
  • where 조건절에서 ',' 단위로 넣어줄 경우 and 조건으로 인식
  • PageImpl 클래스는 Page 인터페이스의 기본 구현체로, 페이징 처리된 결과를 담는 객체

      • pageable : 페이징 정보

      • total : 조건에 맞는 전체 아이템 수

      • content : 실제 조회된 아이템 리스트

 

Querydsl 조회 결과를 반환하는 메소드
메소드 기능
QueryResults<T> fetchResults() 조회 대상 리스트 및 전체 개수를 포함하는 QueryResults 반환
List<T> fetch() 조회 대상 리스트 반환
T fetchOne() 조회 대상이 1건이면 해당 타입 반환.
조회 대상이 1건 이상이면 에러 발생.
T fetchFirst() 조회 대상이 1건 또는 1건 이상이면 1건만 반환
long fetchCount() 해당 데이터 전체 개수 반환. count 쿼리 실행

 

public interface ItemRepository extends JpaRepository<Item, Long>,
        QuerydslPredicateExecutor<Item>, ItemRepositoryCustom {
        
        ...
}
    • ItemRepository 인터페이스에서 ItemRepositoryCustom 인터페이스를 상속
    • QuerydslPredicateExecutor는 Spring Data JPA에서 QueryDSL을 통합하여 제공하는 인터페이스

 

 

ItemService 클래스 코드 추가


  • 상품 조회 조건과 페이지 정보를 파라미터로 받아서 상품 데이터를 조회하는 getAdminItemPage() 메소드 추가
  • 데이터 수정이 일어나지 않으므로 @Transactional(readOnly=true) 어노테이션 설정
    @Override
    @Transactional(readOnly = true)
    public Page<Item> getAdminItemPage(ItemSearchDTO itemSearchDTO, Pageable pageable) {
        return itemRepository.getAdminItemPage(itemSearchDTO, pageable);
    }

 

 

ItemController 클래스 코드 추가


  • 상품 관리 화면 이동 및 조회한 상품 데이터를 화면에 전달하는 로직을 구현
    @GetMapping(value= {"/admin/items", "/admin/items/{page}"})
    public String itemManage(ItemSearchDTO itemSearchDTO,
                             @PathVariable("page")Optional<Integer> page, Model model) {

        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 10);
        Page<Item> items = itemService.getAdminItemPage(itemSearchDTO, pageable);
        model.addAttribute("items", items);
        model.addAttribute("itemSearchDTO", itemSearchDTO);
        model.addAttribute("maxPage", 10);

        return "item/itemMng";
    }

 

  • 페이징을 위해 PageRequest.of 메소드를 통해 Pageable 객체를 생성
  • 첫번째 파라미터로 조회할 페이지 번호, 두 번째 파라미터로 한 번에 가지고 올 데이터 수 입력
  • URL 경로에 페이지 번호가 있으면 해당 페이지를 조회하도록 세팅하고 페이지 번호가 없으면 0페이지를 조회하도록 함
  • 조회 조건과 페이징 정보를 파라미터로 넘겨 Page<Item> 객체를 반환 받음
  • 상품 관리 메뉴 하단에 보여줄 페이지 번호의 최대 개수를 10으로 설정

 

itemMng.html 코드 설정


1.  자바스크립트 코드

$(document).ready(function () {
    $("#searchBtn").on("click", function (e) {
        e.preventDefault(); // 폼 태그 전속 막아줌
        page(0); // 0번째 페이지 조회
    });
});

function page(page) {
// 이동할 페이지 값 받아서 현재 조회조건으로 설정된 키워드를 파라미터로 설정 후 상품데이터 조회

    var searchDateType = $("#searchDateType").val();
    var searchSellStatus = $("#searchSellStatus").val();
    var searchBy = $("#searchBy").val();
    var searchQuery = $("#searchQuery").val();

    location.href = "/admin/items/" + page + "?searchDateType=" + searchDateType
        + "&searchSellStatus=" + searchSellStatus
        + "&searchBy=" + searchBy
        + "&searchQuery=" + searchQuery;
}

 

 

2.  검색 기능 코드

    <!-- form 태그 -->
    <form th:action="@{'/admin/items/' + ${items.number}}"
          role="form" method="get" th:object="${items}">

        <!-- 헤더 부분-->
        <i class="bi bi-clipboard-check-fill fs-1 ms-2" style="color: #FFCFE2;"></i>
        <h2 class="card-title ms-2 mt-1 mb-4 fw-bold text-muted">Item List</h2>

        <!-- 검색 필터 폼 -->
        <div class="form-inline justify-content-center mb-4" th:object="${itemSearchDTO}">
            <select th:field="*{searchDateType}" class="form-control" style="width:auto;">
                <option value="all">Total</option>
                <option value="1d">1 day</option>
                <option value="1w">1 week</option>
                <option value="1m">1 month</option>
                <option value="6m">6 months</option>
            </select>

            <select th:field="*{searchSellStatus}" class="form-control" style="width:auto;">
                <option value="">SELL STATUS(ALL)</option>
                <option value="SELL">SELL</option>
                <option value="SOLD_OUT">SOLD OUT</option>
            </select>

            <select th:field="*{searchBy}" class="form-control" style="width:auto;">
                <option value="itemNm">Product Name</option>
                <option value="createdBy">Writer</option>
            </select>

            <input th:field="*{searchQuery}" type="text" class="form-control w-25"
                   placeholder="Please enter a search word">

            <button id="searchBtn" type="submit" class="btn btn-dark ms-2">Search</button>
        </div>

 

전체적인 동작 방식

   1. 사용자가 검색 조건(날짜, 판매 상태, 기준, 검색어)을 설정하고, 검색 버튼을 클릭

   2. 폼이 제출되면서, URL에 설정된 경로(@{'/admin/items/' + ${items.number}})로 GET 요청이 전송

   3. 서버는 ItemSearchDTO 객체에 바인딩된 폼 데이터를 받아 검색 조건에 맞는 아이템 리스트를 조회

   4. 조회된 결과는 페이징된 형태로 사용자에게 반환되며, 해당 페이지가 렌더링됨

 

 

3.  조회 상품 목록 코드

 <!-- 테이블 구조 -->
        <table class="table">
            <!-- 테이블 헤더 -->
            <thead>
            <tr>
                <th scope="col">Id</th>
                <th scope="col">Product Name</th>
                <th scope="col">Sell Status</th>
                <th scope="col">Writer</th>
                <th scope="col">Date</th>
            </tr>
            </thead>

            <!-- 테이블 바디 -->
            <tbody>
            <tr th:each="item, status: ${items.getContent()}">
                <th scope="col">[[${item.id}]]</th>
                <td>
                    <a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a>
                </td>
                <td th:text="${item.itemSellStatus.toString()}"></td>
                <td th:text="${item.createdBy}"></td>
                <td>[[${#temporals.format(item.regDate, 'yyyy-MM-dd')}]]</td>
            </tr>
            </tbody>
        </table>

 

전체적인 동작 방식

  1. 데이터 바인딩 : items.getContent()에서 가져온 데이터 리스트를 바탕으로 각 아이템의 정보를 테이블 행(tr)으로 출력

  2. 동적 데이터 렌더링 : 각 열(th, td)에 동적으로 데이터를 렌더링하여 아이템의 세부 정보를 표시

  3. 클릭 가능한 링크 : Product Name 열에 아이템 상세 페이지로 이동할 수 있는 링크를 제공

  4. 날짜 포맷팅 : 등록 날짜는 yyyy-MM-dd 형식으로 포맷팅되어 출력

 

 

4.  하단 페이지 번호 코드

 <!-- 페이지 번호의 시작점(start)과 끝점(end)을 계산하여 페이지 번호를 동적으로 생성 -->
        <div th:with="start=${(items.number/maxPage) * maxPage + 1},
            end=(${(items.totalPages == 0) ? 1 : (start + (maxPage - 1) < items.totalPages ?
                start + (maxPage - 1) : items.totalPages)})">

            <ul class="pagination justify-content-center">

                <!-- “Previous” 버튼을 생성 -->
                <li class="page-item" th:classappend="${items.first}?'disabled'">
                    <a th:onclick="'javascript:page(' + ${items.number - 1} + ')'"
                       aria-label='Previous' class="page-link">
                        <span aria-hidden='true'>Prev</span>
                    </a>
                </li>

                <!--동적 페이지 번호 생성-->
                <th:block th:each="i: ${#numbers.sequence(start, end)}">
                    <li th:class="${items.number == i} ? 'page-item active' : 'page-item'">
                        <a class="page-link" th:data-num="${i}">[[${i}]]</a>
                    </li>
                </th:block>

                <!-- “Next” 버튼 생성 -->
                <li class="page-item" th:classappend="${items.last}?'disabled'">
                    <a th:onclick="'javascript:page(' + ${items.number + 1} + ')'"
                       aria-label='Next' class="page-link">
                        <span aria-hidden='true'>Next</span>
                    </a>
                </li>

            </ul>
        </div>

 

start 현재 페이지 번호(items.number)를 기준으로 시작 페이지 번호를 계산. maxPage는 페이지네이션에서 보여줄 최대 페이지 수
end 표시할 마지막 페이지 번호를 계산. items.totalPages는 전체 페이지 수를 나타내며, 시작 번호에서 최대 maxPage만큼 더한 값이 전체 페이지 수를 넘지 않도록 조정.
th:classappend="${items.first}?'
disabled'
현재 페이지가 첫 번째 페이지인지(items.first) 확인하고, 첫 번째 페이지라면 "disabled" 클래스를 추가하여 버튼을 비활성화
th:onclick="'javascript:page(' + ${items.number - 1} + ')'" “Previous” 버튼을 클릭하면 이전 페이지 번호를 가진 page() 함수가 호출
th:each="i: ${#numbers.sequence(start, end)}" start부터 end까지의 숫자 시퀀스를 생성하여, 각 숫자에 대해 반복
th:class="${items.number == i} ? 'page-item active' : 'page-item'  현재 페이지 번호(items.number)와 반복 중인 페이지 번호(i)가 일치하면 active 클래스를 추가하여 현재 페이지를 강조
 th:classappend="${items.last}?'
disabled'
현재 페이지가 마지막 페이지인지(items.last) 확인하고, 마지막 페이지라면 "disabled" 클래스를 추가하여 버튼을 비활성화
 th:onclick="'javascript:page(' + ${items.number + 1} + ')'" “Next” 버튼을 클릭하면 다음 페이지 번호를 가진 page() 함수가 호출

 

 


 

ItemService 코드 추가


  • 등록된 상품을 불러오는 메소드 추가
    @Override
    @Transactional(readOnly = true)
    public ItemFormDTO getItemDtl(Long itemId) {

        // 해당 상품 이미지 조회
        List<ItemImg> itemImgList =
                itemImgRepository.findByItemIdOrderByIdAsc(itemId);

        // 조회한 ItemImg 엔티티를 ItemImgDTO 객체로 만들어 리스트 추가
        List<ItemImgDTO> itemImgDTOList = new ArrayList<>();
        for (ItemImg itemImg : itemImgList) {
            ItemImgDTO itemImgDTO = ItemImgDTO.of(itemImg);
            itemImgDTOList.add(itemImgDTO);
        }

        // 상품 엔티티 조회
        Item item = itemRepository.findById(itemId)
                .orElseThrow(EntityNotFoundException::new);
        ItemFormDTO itemFormDTO = ItemFormDTO.of(item);
        itemFormDTO.setItemImgDTOList(itemImgDTOList);

        return itemFormDTO;
    }

 

  • @Transactional(readOnly = true) : 읽기 전용으로 설정하면 JPA가 더티체킹(변경감지)을 수행하지 않아서 성능을 향상시킬 수 있다.

 

 

ItemController 코드 추가


  • 상품 수정 페이지 진입 위한 코드 추가 - 등록용 페이지와 수정용 페이지 나눠서 개발함.
    @GetMapping( "/admin/item/{itemId}")
    public String itemDtl(@PathVariable("itemId") Long itemId, Model model) {

        try {
            ItemFormDTO itemFormDTO = itemService.getItemDtl(itemId);
            model.addAttribute("itemFormDTO", itemFormDTO);
        } catch (EntityNotFoundException e) {
            model.addAttribute("errorMessage", "The product does not exist.");
            model.addAttribute("itemFormDTO", new ItemFormDTO());
            return "item/itemForm";
        }

        return "item/itemModify";
    }

 

 

ItemImgService 클래스 수정


    @Override
    public void updateItemImg(Long itemImgId, MultipartFile itemImgFile)
            throws Exception {

        if (!itemImgFile.isEmpty()) {
            ItemImg savedItemImg = itemImgRepository.findById(itemImgId)
                    .orElseThrow(EntityNotFoundException::new);

            // 기존 이미지 파일 삭제
            if(!StringUtils.isEmpty(savedItemImg.getImgName())) {
                fileService.deleteFile(itemImgLocation+"/"+savedItemImg.getImgName());
            }

            // 업데이트 파일 업로드
            String oriImgName = itemImgFile.getOriginalFilename();
            String imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
            String imgUrl = "/images/item/" + imgName;
            savedItemImg.updateItemImg(oriImgName, imgName, imgUrl);
        }

    }

 

  • savedItemImg 엔티티는 현재 영속 상태이므로 데이터를 변경하는 것만으로 변경 감지 기능이 동작하여 트랜잭션이 끝날 때 update 쿼리가 실행된다.

 

 

Item 클래스 코드 추가


  • 상품 데이터를 업데이트 하는 로직 추가
    public void updateItem(ItemFormDTO itemFormDTO) {

        this.itemNm = itemFormDTO.getItemNm();
        this.price = itemFormDTO.getPrice();
        this.stockNum = itemFormDTO.getStockNum();
        this.itemDetail = itemFormDTO.getItemDetail();
        this.itemSellStatus = itemFormDTO.getItemSellStatus();

    }

 

 

ItemService 업데이트 코드 추가


    @Override
    public Long updateItem(ItemFormDTO itemFormDTO, List<MultipartFile> itemImgFileList)
            throws Exception {

        // 상품 수정
        Item item = itemRepository.findById(itemFormDTO.getId())
                .orElseThrow(EntityNotFoundException::new);
        item.updateItem(itemFormDTO);

        List<Long> itemImgIds = itemFormDTO.getItemImgIds();

        // 이미지 등록
        for (int i=0; i<itemImgIds.size(); i++) {
            itemImgService.updateItemImg(itemImgIds.get(i), itemImgFileList.get(i));
        }

        return item.getId();
    }

 

  • 상품 등록 화면으로부터 전달 받은 상품 아이디를 이용하여 상품 엔티티 조회
  • 상품 등록 화면으로부터 전달 받은 ItemFormDTO를 통해 상품 엔티티를 업데이트
  • 상품 이미지 아이디 리스트 조회
  • 상품 이미지 업데이트 위해 updateItemImg() 메소드에 상품 이미지 아이디와 상품 이미지 파일 정보를 파라미터로 전달

 

ItemController 코드 추가


  • 상품 수정하는 URL을 ItemController 클래스에 추가
    @PostMapping("/admin/item/{itemId}")
    public String itemUpdate(@Valid ItemFormDTO itemFormDTO,
                             BindingResult bindingResult,
                             @RequestParam("itemImgFiles") List<MultipartFile> itemImgFileList,
                             Model model) {

        if (bindingResult.hasErrors()) {
            return "item/itemModify";
        }

        if (itemImgFileList.get(0).isEmpty() && itemFormDTO.getId() == null) {
            model.addAttribute("errorMessage", "The first product image is a required field.");
            return "item/itemModify";
        }

        try {
            itemService.updateItem(itemFormDTO, itemImgFileList);
        } catch (Exception e) {
            model.addAttribute("errorMessage", "An error occured during product registration.");
            return "item/itemForm";
        }

        return "redirect:/";

    }

 

 

 

 

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

+ Recent posts