Programming/Java

Set 계열, Map 계열, DTO, VO

김심슨 2025. 5. 28. 14:29

1. set 계열 

- 중복, 순서 X 

- 하나의 집합

 

set이라는 인터프리터를 이용해 만든 클래스들 

- HashSet : 순서를 유지하지 않음, 가장 일반적인 Set

- LinkedHashSet : 입력 순서를 유지, 메모리를 연결-연결 해서 쓰는 거라서 순서 유지함 

- TreeSet : 자동 정렬 (오름차순), 내부적으로 트리 구조 사용 (이진 트리 기본)

 

- 트리구조 : 검색이 빠름 

배열구조로 저장이 되면 인덱스를 모를 경우 다 뒤져봐야해서 속도가 느리다. 

insert 하거나 sort 할 땐 느릴 수 있지만 검색할 땐 속도가 반으로 줄어든다. 

 

예제 : HashSet

package lesson07;

import java.util.HashSet;

public class Ex01_Main {
    public static void main(String[] args) {
        HashSet<String> set = new HashSet<String>(); // 고정된 길이를 갖는다. hash 알고리즘을 사용한다. set 중복되지 않음
        set.add("김사과");
        set.add("반하나");
        set.add("오렌지");
        set.add("이메론");
        set.add("배애리");
        // 중복 데이터 : 추가되지 않음 : [반하나, 이메론, 김사과, 오렌지, 배애리]
        set.add("김사과");
        set.add("배애리");

        System.out.println(set);
        // [반하나, 이메론, 김사과, 오렌지, 배애리] : 순서가 없는 모습

        // 내부적으로 순서가 없더라도 반복문은 돌릴 수 있는 자료구조임
        System.out.println("출석한 학생 명단 ");
        for(String name : set) {
            System.out.println("- " + name);
        }
        // 출석한 학생 명단 
        //- 반하나
        //- 이메론
        //- 김사과
        //- 오렌지
        //- 배애리

        // 데이터 있는 지 찾아보기 : contains
        System.out.println("오렌지 출석 여부 : "+ set.contains("오렌지")); //오렌지 출석 여부 : true
        System.out.println("박수박 출석 여부 : "+ set.contains("박수박")); //박수박 출석 여부 : false
        
        // 삭제
        set.remove("이메론");
        for(String name : set) {
            System.out.println("- " + name);
        }
        //- 반하나
        //- 김사과
        //- 오렌지
        //- 배애리
        
        // 자바스크립트, json은 key : value가 있다. set은 순서가 없기 때문에 인덱스도 없고, key도 없다. 그래서 value를 꺼낼 방법이 없어서 value 자체로 찾아낸다. (오렌지, 박수박 직접 입력)
    }
}

 

예제 : TreeSet

package lesson07;

import java.util.TreeSet;

public class Ex02_Main {
    public static void main(String[] args) {
        TreeSet<String> set = new TreeSet<String>();
        // 저장과 삭제는 느릴 수 있지만, 검색이 빠르다. 정렬도 함.
        set.add("김사과");
        set.add("반하나");
        set.add("오렌지");
        set.add("이메론");
        set.add("배애리");

        set.add("김사과");
        System.out.println(set);
        // [김사과, 반하나, 배애리, 오렌지, 이메론] : 중복데이터를 넣었지만, 추가 X , 오름차순으로 정렬
        // 정렬되는 상황? add 될 때 정렬을 하고 들어감 (그래서 insert 될 때 느린 것)

        for (String name : set) {
            System.out.println("- " + name);
            //- 김사과
            //- 반하나
            //- 배애리
            //- 오렌지
            //- 이메론
        }
        // 앞에 오는 값 : lower, 뒤에오는 값 : higher
        System.out.println("오렌지보다 앞에 있는 이름 찾기 : " + set.lower("오렌지")); // 배애리
        System.out.println("오렌지보다 뒤에 있는 이름 찾기 : " + set.higher("오렌지")); // 이메론
        // key:value가 없기 때문에 value 자체로 찾아야 함

        // 만약 내림차순을 하고 싶다면? Comparable을 오버라이딩 하면 됨
        // Comparable이란? 

    }
}

* Comparble이란?

자바에서 객체를 정렬 가능하게 만들기 위한 인터페이스 

public interface Comparable<T> {
    int compareTo(T o);
}

핵심 목적 : sort(), priorityQueue, TreeSet 같은 자료 구조에서 객체끼리 순서를 비교할 수 있게 하기 위해 쓰임 

예제 :

public class Student implements Comparable<Student> {
    String name;
    int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    // 오름차순 정렬 (score 기준)
    @Override
    public int compareTo(Student other) {
        return this.score - other.score;  // 낮은 점수부터 정렬됨
    }
    
    @Override
	public int compareTo(Student other) {
    return other.score - this.score;  // 높은 점수부터 정렬됨
}
}

 

* 빅오 표기법이란?

알고리즘의 효율성을 표기해주는 표기법 

알고리즘의 효율성은 데이터 개수(n)가 주어졌을 때 덧셈, 뺄셈, 곱셈 같은 기본 연산의 횟수를 의미 

시간복잡도, 공간 복잡도를 나타내는데 주로 사용됨 

n이 상수에 가까울 수록 빠르고 좋은 것.

 

예제 : 이름으로 내림차순 

package lesson07;

import java.util.Comparator;
import java.util.TreeSet;

class Ex04_Student  {
    String name;
    int score;
    // 생성자 만들기
    public Ex04_Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return name + "(" + score + "점)";
    }
}

public class Ex04_MAin {
    public static void main(String[] args) {
        Comparator<Ex04_Student> comparator= (a,b) -> b.name.compareTo(a.name);
        TreeSet<Ex04_Student> students = new TreeSet<>(comparator);
        // <> 안에 들어갈 수 있는 건 레퍼 클래스 등을 넣었는데, Student객체를 넣으면 트리셋에 쌓일 것임. Student에는 이름,점수 2개가 들어있음 그렇다면 뭐로 정렬을 시킬까?
        students.add(new Ex04_Student("김사과", 90));
        students.add(new Ex04_Student("반하나", 85));
        students.add(new Ex04_Student("오렌지", 95));
        students.add(new Ex04_Student("이메론", 92));
        students.add(new Ex04_Student("배애리", 85));
        // 어떻게 들어가있는지 확인해보자
        for (Ex04_Student student : students) {
            System.out.println("- " + student);
        }
    }
}

 

2. Map 계열 (많이 사용함)

- 데이터를 key-value 쌍으로 저장 

-Key는 중복될 수 없음, Value는 중복 가능 

- 값을 검색할 땐 key를 사용 (map.get("name"))

- userid라는 키를 만들어서 아이디를 저장하거나, userpw를 만들어서 pw를 저장할 때 사용한다. 

package lesson07;

import java.util.HashMap;
import java.util.Map;

public class Ex05_Main {
    public static void main(String[] args) {
        HashMap<String, Integer> scoreMap = new HashMap<>();
        scoreMap.put("김사과", 90);
        scoreMap.put("반하나", 85);
        scoreMap.put("오렌지", 95);
        scoreMap.put("이메론", 88);
        scoreMap.put("배애리", 92);

        System.out.println(scoreMap.entrySet());
        // [반하나=85, 이메론=88, 김사과=90, 오렌지=95, 배애리=92] : key=value 형태로 저장하고 있음
        for(Map.Entry<String, Integer> entry : scoreMap.entrySet()){
            // entry 저장 방식 key=value
            System.out.println(entry.getKey() + ": " + entry.getValue()+"점"); // key, value를 따로 갖고 나옴
            //반하나: 85점
            //이메론: 88점
            //김사과: 90점
            //오렌지: 95점
            //배애리: 92점
        }
        String name = "오렌지";
        System.out.println(name + "의 점수: " + scoreMap.get(name));
        // get 안에 오렌지를 넣어주게 되면, 점수만 출력됨

        // 만약, map에 key를 동일한 걸 집어 넣게 되면? update 된다.
        scoreMap.put("김사과",100);
        System.out.println("김사과의 점수 수정 후 : " + scoreMap.get("김사과"));

        scoreMap.remove("반하나");
        for(Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
            // entry 저장 방식 key=value
            System.out.println(entry.getKey() + ": " + entry.getValue() + "점"); // key, value를 따로 갖고 나옴
            // 반하나 없어짐
        }
        System.out.println("전체 학생 수: " + scoreMap.size() + "명");
    }
}

 

3. DTO = Data Transfer Object 

데이터를 담아서 전달하는 용도의 객체 

=> 데이터만 들고 다니는 택배상자 같은 것 

- 로직(기능)은 없음 

- 데이터 필드만 다님 (필드+getter/setter + 생성자)

 

왜 쓰는가?

1. 계층 간 데이터 전달용 

- controller -> service -> Repository

이런 구조에서 중간중간 필요한 데이터만 뽑아서 전달 

2. DB나 외부 API에서 받은 데이터를 포장해서 사용 

- DB에서 받은 Entity를 그대로 노출하지않음 (보안/성능 문제)

필요한 정보만 담아서 DTO로 전달함

 

예 : 회원정보 DTO

public class MemberDTO {
    private String name;
    private int age;
    private String email;

    // 생성자, getter, setter
}

- 이 객체는 그냥 name, age, email을 담아서 넘기기만 하는 용도 (비즈니스 로직, 계산, 검증 같은 건 안함)

 

예 : DTO가 아닌 예 (비교)

public class Member {
    private String name;
    private int age;

    public boolean isAdult() {
        return this.age >= 20;
    }
}

- 데이터 + 로직이 있기 때문에 DTO가 아님 

 

예제 : 단어장

package lesson07;

/*

[사용자 입력] > controller > service > Repo > DTO 저장

*/

import java.time.LocalDate;
import java.util.*;

// 데이터 상자 역할, 전달 전용 객체, DB처럼 값을 담고 주고받을 때 사용함.
class WordDTO {
    // 데이터 베이스에 저장할 내용이 있다면 그거랑 동일하게 필드를 구성하는 객체 이면서, 이걸 서비스 단으로 주고받고 할겨
    private String word; //영단어
    private String meaning; // 한국어
    private String level;
    private LocalDate regDate;
    // private로 막아뒀기 때문에 getter, setter로 가져와서 써야함.

    // 생성자 만들기 (마우스 우클릭으로 다 선택해서 만들어버령)
    public WordDTO(String word, String meaning, String level) {
        this.word = word;
        this.meaning = meaning;
        this.level = level;
        this.regDate = LocalDate.now();
    }

    // getter/setter도 마우스 우클릭으로 다 만들기
    public String getWord() {
        return word;
    }
    public void setWord(String word) {
        this.word = word;
    }

    public String getMeaning() {
        return meaning;
    }
    public void setMeaning(String meaning) {
        this.meaning = meaning;
    }

    public String getLevel() {
        return level;
    }
    public void setLevel(String level) {
        this.level = level;
    }

    public LocalDate getRegDate() {
        return regDate;
    }
    public void setRegDate(LocalDate regDate) {
        this.regDate = regDate;
    }

    @Override
    public String toString() {
        return "[단어] : " + word + " | 뜻 : " + meaning + " | 레벨 : " + level + " | 등록일 : " + regDate;
    }
}

// 사용자 요청을 처리하는 역할 (등록을 받으면, wordService에 전달, 2번의 요청도 여기서 받음)
class WordController {
    private final WordService service = new WordService();

    // 단어 등록
    public void register(String word, String meaning, int levelNum) {
        String level = ConvertLevel(levelNum);
        // 레벨부터 필터링
        if(level == null) {
            System.out.println("잘못된 레벨입니다.(1: 초급, 2: 중급, 3: 고급)");
            return;
        }
        try{
            WordDTO dto = new WordDTO(word, meaning, level);
            service.registerWord(dto);

        } catch(IllegalArgumentException e) { //파라미터가 문제 있다면
            System.out.println("등록 실패 : " + e.getMessage());
        }
    }
    // 전체 단어 조회
    public void printAllWords() {
        List<WordDTO> list = service.getAllwords();
        if(list.isEmpty()) {
            System.out.println("등록된 단어가 없습니다");
        } else {
            System.out.println("등록된 단어 목록");
            for(WordDTO dto : list) {
                System.out.println("- " + dto);
            }
        }
    }

    // 단어 상세 조회
    public void getWord(String word) {
        WordDTO dto = service.getWord(word);
        if(dto == null) {
            System.out.println(word + " 단어는 등록되어있지 않습니다.");
        } else {
            System.out.println("- " + dto);
        }
    }


    // 레벨에 대한 함수 (그냥 만듦)
    private String ConvertLevel(int levelNum) {
        return switch (levelNum) {
            case 1 -> "초급";
            case 2 -> "중급";
            case 3 -> "고급";
            default -> null;
        };
    }
}
// controller와 Repository 사이에서 중간 다리 역할
class WordService {
    private final WordRepository repository = new WordRepository();
    // 단어 등록 : 유효성 검사 하고 Repo로 전달
    public void registerWord(WordDTO dto) {
        if(dto.getWord().isBlank() || dto.getMeaning().isBlank()) {throw new IllegalArgumentException("단어와 뜻은 반드시 입력해야 합니다.");}
        repository.save(dto);
    }
    // 전체 단어 조회 : controller에서 요청 오면 바로 Repo에 요청
    public List<WordDTO> getAllwords() {
        return repository.findAll();
    }
    // 단어 상세 조회
    public WordDTO getWord(String word) {
        return repository.findWord(word);
    }

}

// 임시 DB 역할 : 나중에 DB 연결 시 이 부분만 바꾸면 됨. 실제로 단어를 저장하고 불러오는 역할
class WordRepository {
    private final Map<String, WordDTO> wordMap = new HashMap<>();
    // 단어 저장
    public void save(WordDTO dto) {
        wordMap.put(dto.getWord(), dto);
        System.out.println("저장 완료: " + dto.getWord());
    }
    // 단어 전체 조회하는 것 (value들만 뽑아서 array에 담아줄게)
    public List<WordDTO> findAll() {
        return new ArrayList<>(wordMap.values());
    }
    public WordDTO findWord(String word) {
        return wordMap.get(word);
    }
}

// 프로그램 시작점 + 사용자 입력 받는 콘솔 UI
public class Ex06_Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        WordController controller = new WordController();

        while(true) {
            System.out.println("영어 단어 사전");
            System.out.println("1. 단어 등록");
            System.out.println("2. 전체 단어 목록 보기");
            System.out.println("3. 단어 상세 조회하기");
            System.out.println("0. 종료하기");
            System.out.print("선택 : ");
            int choice = Integer.parseInt(sc.nextLine());

            switch(choice) {
                case 0 -> {
                    System.out.println("종료");
                    sc.close();
                    return; //메인 메소드 빠져나가기
                }
                case 1 -> {
                    System.out.print("영단어 입력: ");
                    String word = sc.nextLine();

                    System.out.print("뜻 입력: ");
                    String meaning = sc.nextLine();

                    System.out.print("레벨 선택 (1: 초급, 2: 중급, 3: 고급): ");
                    int level = Integer.parseInt(sc.nextLine());

                    controller.register(word, meaning, level);
                }
                case 2 -> {
                    controller.printAllWords();
                }
                case 3 -> {
                    System.out.println("조회할 영단어 입력 :");
                    String word = sc.nextLine();
                    controller.getWord(word);
                    // 입력한 단어만 출력하는 것
                }
                default -> System.out.println("잘못된 메뉴 입니다");
            }
        }
    }
}

 

4. VO : 값을 표현하기 위한 객체 

- 변하지 않는 고정된 정보

- 비교 기준이 '주소'가 아닌 '값' 

 

특징 3가지 

- 불변성 : 한 번 만들면 값을 바꿀 수 없음 (setter 없음)

- 값 기반 비교 : 주소(==)가 아닌 값(.equals())으로 비교 

- 재사용 가능 : 같은 값이면 다른 객체로 만들어도 같다고 침 

 

예제 : Money라는 VO

public class Money {
    private final int amount;

    public Money(int amount) {
        this.amount = amount;
    }

    public int getAmount() {
        return amount;
    }

    // 값 기반 비교를 위해 equals & hashCode 구현
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount == money.amount;
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount);
    }
}


--------------------------------------------------------------------------------------
Money m1 = new Money(1000);
Money m2 = new Money(1000);

System.out.println(m1 == m2);        // false (주소 다름)
System.out.println(m1.equals(m2));   // true  (값 같음)

이걸 왜 만들었나?

- 돈이라는 값을 안전하고 명확하게 다루기 위해서 

🤔 왜 이렇게까지 만들어야 하나요?

단순히 숫자만 사용하면 다음과 같은 문제가 발생할 수 있습니다

int price = 1000; // 이게 가격인가요? 나이인가요? 개수인가요?
 

반면 Money 객체는 다음처럼 의도가 명확해집니다.

Money price = new Money(1000); // '1000원'이라는 의미

 

예제 : 배달 가능 지역 확인 시스템 -  VO 먼저 만들기 

package lesson07;

import java.util.Objects;

class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public double distance(Point other) {
        // 현재 내가 저장하고 있는 x값과 받은 x값을 빼서 dx가 나옴
        int dx = this.x - other.x;
        int dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy); // 거리 구하는 피타고라스 공식
    }
    public int getX() {return x;}
    public int getY() {return y;}

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; //메모리 위치가 같으면 true
        if (!(obj instanceof Point)) return false; // 클래스 자체가 다른 걸로 만들어졌다면 비교할 것도 없어요! 같은 클래스거나 얘를 상속받아 만든거면 true가 나오게 되어있는데 그게 아니라면 false라고 나옴
        Point other = (Point) obj; // 다운 캐스팅
        return x == other.x && y == other.y; //x값이 other.x와 같고.... 둘다 완벽하게 맞았을 때만 true, 아니면 false
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

public class Ex07_Main {
}

 

- StoreService, main 만들기 

class StoreService{
    private final Point storeLocation = new Point(0, 0);
    public boolean canDeliver(Point customerLocation) {
        double distance = storeLocation.distance(customerLocation);
        System.out.println("거리계산 : " + distance);
        return distance <= 5.0;
    }
}

public class Ex07_Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        StoreService service = new StoreService();
        System.out.println("가게 기준점은 (0,0)입니다.");
        System.out.print("고객 위치 x 좌표를 입력 : ");
        int x = Integer.parseInt(sc.nextLine());
        System.out.print("고객 위치 y 좌표를 입력 :  ");
        int y = Integer.parseInt(sc.nextLine());

        Point customer = new Point(x, y);
        System.out.println("고객 위치 : " + customer);

        if(service.canDeliver(customer)) {
            System.out.println("배달 가능 지역입니다.");
        } else {
            System.out.println("배달 불가능 지역입니다.");
        }
        sc.close();
    }
}

 

+ ) DTO와 VO의 차이점 

 

항목                          DTO (Data Transfer Object)                             VO (Value Object)

목적 데이터 전달용 (입·출력) 값 표현용 (의미 있는 단위)
변경 가능성 있음 (setter 사용) ❌ 없음 (Immutable)
비교 기준 객체 자체 (주소 기준) 값 기준 (equals())
예시 UserDTO, OrderDTO Money, Coordinate, Email, Address
사용하는 곳 Controller ↔ Service ↔ 외부 Entity, Domain 내부

 

DTO가 택배 박스라면, VO는 신분증 (이게 누구인지 증명하는 고유 정보) 이라고 할 수 있다.