객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.
일급 컬렉션

 

 

일급 컬렉션이란?

Collection을 Wrapping하면서 그 외의 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 합니다.

일급컬렉션의 장점은 아래와 같습니다.

  1. 비즈니스의 종속적인 자료구조
  2. 불변성 보장
  3. 상태와 행위를 한 곳에서 관리
  4. 이름이 있는 컬렉션

 

@Getter
public class Student {

    private Name name;
    private Age age;

    public Student(Name name, Age age) {
        this.name = name;
        this.age = age;
    }
    
}

 

이전 원시값 포장에 대해서 설명할때 사용되었던 Student 객체입니다.

 

이번에는 학생명단을 만들어보겠습니다.

  • 학생명단은 최대 10명
  • 이름은 중복되지 않아야함

예시를 위해서 두가지의 검증로직이 필요하다고 가정하겠습니다. 그리고 이 검증로직을 Service 계층에서 진행했습니다.

 

public class StudentService {

    public void addStudent(List<Student> students, Student student) {
        validateStudentsSize(students);
        validateStudentName(students, student);

        students.add(student);
    }

    private void validateStudentName(List<Student> students, Student student) {
        String name = student.getName().getName();

        for (Student studentFor : students) {
            String nameFor = studentFor.getName().getName();

            if (name.equals(nameFor)) {
                throw new IllegalStudentsException("중복된 이름이 존재합니다.");
            }
        }
    }

    private void validateStudentsSize(List<Student> students) {
        if (students.size() > 10) {
            throw new IllegalStudentsException("최대 학생 수는 10명입니다.");
        }
    }
}

 

비즈니스로직을 모두 처리했지만 약간의 문제가 있습니다.

  1. List 컬렉션에 의존적입니다.
  2. List<Student>와 StudentService간에 결합도가 높습니다.
  3. 다른 사람이 보았을 때 List<Student>의 일을 파악하기 어렵습니다.
  4. 컬력션이 변경된다면 OCP원칙에 위배됩니다.

이러한 직접적인 의존성은 프로그램이 변화하거나 성장할 때 유연성을 제한하며, List<Student>의 변경은 StudentService 클래스의 범위를 넓혀 유지보수를 어렵게 만듭니다. 이런 구조는 코드 변경에 취약하며, 새로운 데이터 구조나 변경 사항이 발생할 때마다 Service 로직을 수정해야 하는 번거로움이 발생할 수 있습니다. 또한 이 문제는 개방-폐쇄원칙(OCP 원칙)을 위배하게 됩니다.

 

이제 이 문제들을 해결해 보겠습니다.

 

 

 

1. 비즈니스의 종속적인 자료구조

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }

    public void add(Student student) {
        validateStudentsSize();
        validateStudentName(student);

        students.add(student);
    }
    
    public List<Student> getStudents() {
        return Collections.unmodifiableList(students);
    }

    private void validateStudentName(Student student) {
        String name = student.getName().getName();

        for (Student studentFor : students) {
            String nameFor = studentFor.getName().getName();

            if (name.equals(nameFor)) {
                throw new IllegalStudentsException("중복된 이름이 존재합니다.");
            }
        }
    }

    private void validateStudentsSize() {
        if (students.size() > 10) {
            throw new IllegalStudentsException("최대 학생 수는 10명입니다.");
        }
    }
}

 

StudentList 클래스를 도입함으로써 List<Student> 객체를 캡슐화 했고, Service에서 관리하던 비즈니스로직을 StudentList 클래스로 종속시켰습니다이 변경으로 인해 StudentService(외부에서)는 더 이상 직접적으로 List<Student>를 다루지 않고, StudentList의 메소드를 통해 학생 목록을 조작할 수 있게 되었고, 

 

따라서 컬렉션 변경에 대한 OCP원칙을 지킬 수 있게 되었고, 학생명단과 StudentService간에 결합도가 낮아졌습니다.

 

 

 

2. 불변성 보장

일급 컬렉션은 컬렉션의 불변을 보장하는데, 단순히 final 을 사용하는 것이 아니라 캡슐화를 통해 이뤄집니다.

* final은 재할당만 금지하는 것이므로 add, remove 등이 가능합니다.

 

List<Student> 객체는 StudentList 클래스 내부에 있기때문에 Student 하위에 setter가 존재하지 않는 한 값의 변경이 일어날 수 없습니다. 불변성에 대한 자세한 내용은 차후 다룰 예정입니다.

 

public List<Student> getStudents() {
    return Collections.unmodifiableList(students);
}

 

// getStudents : Collections.unmodifiableList(students);
List<Student> students = studentList.getStudents();
Student newStudent = new Student(new Name("이OO"), new Age(21));
students.add(newStudent); // UnsupportedOperationException 런타임 예외발생!

 

 

3. 상태와 행위를 한 곳에서 관리

일급컬렉션은 값과 로직이 한 객체안에 있기때문에 응집도가 높아집니다. 따라서 변경이 용이하고 유지보수가 수월해집니다.

 

만약 비즈니스 로직이 추가된다면?

Service에서 로직을 추가할 경우 결합도만 높아지게 되고 수정사항이 증가하게 될겁니다.

하지만 일급컬렉션을 사용한다면 클래스 내부에서 수정 및 추가만 하면 됩니다.

 

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }
	
    // 만약 나이의 평균을 구하는 로직이 필요하다면? 
    public double averageAge() {
        return students.stream()
            .mapToInt(student -> student.getAge().getAge())
            .average()
            .orElse(0);
    }
	
    // 나머지 메서드 ...

}

 

 

4. 이름이 있는 컬렉션

List<Student> 만으로 그 의미를 추론하기는 어렵습니다. 하지만 StudentList 또는 그 상황에 맞는 이름을 붙혀준다면 그 의미는 명확해지고, 다른 사람이 코드를 분석할 때 객체 이름으로 어떤일을 할지 파악하기 쉬워진다는 겁니다.

 

public static void main(String[] args) {

    // 같은 List<Payment> 이기 때문에 ,
    // List<Payment>를 매개변수로 받는 메서드에는 모두 들어갈 수 있습니다.
    List<Payment> kakaoPay = createPayments();
    List<Payment> naverPay = createPayments();

    // 객체가 다르다면 사용용도가 명확해집니다.
    KakaoPay kakaoPay = createKakaoPay();
    NaverPay naverPay = createNaverPay();

}

 

 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.

 

모든 원시 값과 문자열을 포장한다.

 

쉽게말해 int, String 과 같은 타입의 값을 객체로 포장하는겁니다.

 

public class Student {

    private String name;
    private int age;

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

 

Student 객체에는 이름과 나이를 가지고 있습니다.

 

만약 이름은 최소 2자 이상, 나이는 20세 이상 이라는 조건이 추가된다면?

 

public class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        validName(name);
        validAge(age);
        this.name = name;
        this.age = age;
    }

    private void validName(String name) {
        if (name.length() < 2) {
            throw new IllegalStudentNameException("이름은 최소 2자 이상이어야 합니다.");
        }
    }
    private void validAge(int age) {
        if (age < 20) {
            throw new IllegalStudentAgeException("나이는 20살 이상이어야 합니다.");
        }
    }

}

 

잘못된 값의 검증을 제외한 일반적인 예외처리를 했음에도 Student가 할 일이 엄청 늘어났습니다.

Student는 이제 이름과 나이에 대한 상태관리를 모두 해야합니다.

그럼 만약 Student에 이메일, 성별 등 변수가 추가된다면 코드는 복잡해질것입니다.

 

이제 원시 타입 변수로 포장해보겠습니다.

public class Student {

    private Name name;
    private Age age;

    public Student(Name name, Age age) {
        this.name = name;
        this.age = age;
    }
}

public class Name {

    private String name;

    public Name(String name) {
        validName(name);
        this.name = name;
    }

    private void validName(String name) {
        if (name.length() < 2) {
            throw new IllegalStudentNameException("이름은 최소 2자 이상이어야 합니다.");
        }
    }

}

public class Age {

    private int age;

    public Age(int age) {
        validAge(age);
        this.age = age;
    }

    private void validAge(int age) {
        if (age < 20) {
            throw new IllegalStudentAgeException("나이는 20살 이상이어야 합니다.");
        }
    }

}

 

이제 Student에서 이름과 나이에 대한 책임이 분리 되면서 단일 책임 원칙(SRP)도 지키게 되었습니다.

제네릭이란?

이 글을 보러 오신분들은 대부분 자바를 공부하면서 제네릭이 뭔지 궁금해서 들어온 분들이실겁니다. 

제네릭(generic)이란 데이터의 타입을 일반화한다(generalize)는 것을 의미합니다.

이렇게 적으면 아마 이해가 어려우실겁니다.

 

 

사실 우리가 흔히 사용하는 List, Map, Set 등의 컬렉션에서 자주 볼 수 있습니다.

 

public static void main(String[] args) {

        List<String> stringList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        Set<Double> set = new HashSet<>();
        
}

 

객체<타입> 객체명 = new 객체<타입>();

 

이 형식에서 < > 이것이 바로 제네릭 입니다.

 

즉 제네릭은 클래스 외부에서 사용자에 의해 타입을 지정되는 것 ( 타입을 일반화한다 ) 을 의미합니다.

 

 

제네릭의 특징

  1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.
  2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
  3. 잘못된 타입이 입력되는 것을 컴파일 단계에서 방지할 수 있다.
  4. 재사용성이 높다.

 

제네릭 사용방법

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value

이 표현식은 관례적으로 사용되고 있습니다. 반드시 일치해야할 필요도 없고 한 글자일 필요도 없습니다.

하지만 개발은 혼자하는것이 아니기에...

 

 

public interface Generic <T> { ... }
public class Generic <T> { ... }
public class Generic <T, K> { ... }

클래스와 인터페이스는 이렇게 선언하면 됩니다.

 

이번에는 List와 같은 클래스를 하나 만들어 보겠습니다.

 

제네릭 클래스

public class Generic <T> {

    private final List<T> list = new ArrayList<>();


    public void add(T value) {
        list.add(value);
    }

    public void remove(T value) {
        list.remove(value);
    }

    public T get(int index) {
        return list.get(index);
    }

}

public static void main(String[] args) {

    Generic<String> generic = new Generic<>();

    String test = "제네릭";
    generic.add(test);
    generic.remove(test);

    int size = generic.size();
    System.out.println(size); // 0
}

 

만약 String 이 아닌 타입이 입력된다면?

public static void main(String[] args) {

    Generic<String> generic = new Generic<>();

    int test = 1;
    generic.add(test); // 컴파일 에러!
    generic.remove(test); // 컴파일 에러!

    int size = generic.size();
    System.out.println(size);
}

컴파일 과정에서 오류를 방지할 수 있습니다.

 

제네릭 메서드

// [접근제어자] <제네릭타입> [반환타입] [변수명] ( [제네릭타입] 매개변수명 )
public <T> T getMethod(T value) { ... }

 

 

 

 

 

와일드 카드 < ? >

위에서의 제네릭에서는 타입을 제한하지 않았습니다. 와일드카드는 타입의 제한을 둘 수 있습니다.

<K extends T> { ... } // T, T의 자식 타입만 가능
<K super T> { ... } // T, T의 부모 타입만 가능

<? extends T> { ... } // T, T의 자식 타입만 가능
<? super T> { ... } // T, T의 부모 타입만 가능

 

여기서 K extends T와 ? extends T 는 차이가 있습니다.

K는 타입이 지정이 되지만 ?는 타입이 지정되지 않습니다.

 

여기서 주의점은 와일드카드는 클래스에 사용할 수 없다는 것입니다. 클래스에서는 타입을  <K extends T>와 같이 지정해주어야합니다.

 

public class Parent{
    
    public void call() {
        System.out.println("Parent Call");
    }
}

public class Children extends Parent{

    @Override
    public void call() {
        System.out.println("Children Call");
    }
}

public class Main {

    public static void main(String[] args) {
        List<Parent> list = new ArrayList<>();

        list.add(new Parent());
        list.add(new Children());

        listCall(list);
    }

    private static void listCall(List<? extends Parent> list) {
        for (Parent parent : list) {
            parent.call();
        }
        // Parent Call 출력
        // Children Call 출력
    }
}

 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.

 

 

 

else 예약어를 사용하지 않는다.

 

if-else에서 else 예약어를 사용하지 않는다는 의미입니다. switch문도 허용하지 않습니다.

 

public class ex2 {

    public static void main(String[] args) {

        String select = "1";

        String payment = getPayment(select);
        System.out.println(payment); // "신용카드"
    }

    private static String getPayment(String select) {
        String payment = null;

        if ("1".equals(select)) {
            payment = "신용카드";
        } else if ("2".equals(select)) {
            payment = "무통장입금";
        } else if ("3".equals(select)) {
            payment = "카카오페이";
        } else {
            payment = "네이버페이";
        }
        return payment;
    }
}

 

if-else 문을 사용하여 코드를 작성했습니다. 겉보기에는 가독성도 괜찮고 전혀 문제될것이 없는 코드 같습니다. 하지만 실제 로직을 작성한다면 이보다 훨씬 복잡한 코드를 작성하게 될것입니다.

 

이 코드의 비즈니스적으로 문제가 있습니다. 사용자가 1,2,3,4가 아닌 다른 값을 넣었다면? 개발자는 예외가 발생하길 원하지만 실제로는 "네이버페이"가 정상적으로 반환될것입니다. 이는 설계의도를 완전히 벗어난 결과입니다. 이제 early return 구조를 적용해보겠습니다.

 

public class ex2 {

    public static void main(String[] args) {

        String select = "5";

        String payment = getPayment(select); // NotFoundPaymentException 예외발생!!
        System.out.println(payment);
    }

    private static String getPayment(String select) {

        if ("1".equals(select)) {
            return "카드";
        }
        if ("2".equals(select)) {
            return "무통장입금";
        }
        if ("3".equals(select)) {
            return "카카오페이";
        }
        if ("4".equals(select)){
            return "네이버 페이";
        }
        throw new NotFoundPaymentException("잘못된 결제방식입니다.");
    }
}

 

구조를 살짝 바꾸면서 메서드의 가독성이 높아졌습니다. 개발자가 의도한대로 조건에 만족한다면 결제방식을 바로 반환하며 종료됩니다. 반대로 조건에 만족하는 결과가 없다면 예외를 발생시키면서 설계의도대로 동작할것입니다. 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.

 

 

한 메서드에 오직 한 단계의 들여쓰기만 한다.

 

들여쓰기의 깊이를 2 이상 두지 말라는 지침입니다. for 문안에 if문이 있으면 깊이가 2인 코드가 됩니다. 따라서 한 메서드 안에 깊이가 2 이상인 코드가 존재하면 안된다는 말과 같습니다.

 

이 원칙의 의미는 코드를 작성할때 무조건 들여쓰기를 지양하라는 것보다는 메서드를 분리함으로써 하나의 메서드가 하나의 일을 하도록 설계하자는데 의의가 있습니다. 일을 잘개 쪼갤수록 결합도는 낮아지고, 응집도는 높아집니다. 이러한 코드는 재사용성이 높고, 디버깅에 용이해집니다. 따라서 한 메서드에는 한가지의 주요 작업을 수행하도록 구성하는 것이 좋습니다.

 

public class ex1 {

    static List<Integer> boardNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

    public static void main(String[] args) {
        int sum = sumNumbers();
        System.out.println(sum);
    }

    private static int sumNumbers() {
        int sum = 0;
        for (int number : boardNumbers) {
            if (number > 5) {
                sum += number;
            }
        }
        return sum;
    }
}

 

boardNumbers에서 5 이상인 수만 더해서 반환하는 메서드 입니다. for 문 안에 if문이 있다는건 깊이가 2라는 의미입니다.

메서드를 분리해보겠습니다.

 

public class ex1 {

    static List<Integer> boardNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

    public static void main(String[] args) {
        int sum = sumNumbers();
        System.out.println(sum);
    }

    private static int sumNumbers() {
        int sum = 0;
        for (int number : boardNumbers) {
            sum += check(number);
        }
        return sum;
    }

    private static int check(int number) {
        if (number > 5) {
            return number;
        }
        return 0;
    }
}

 

메서드를 분리함으로써 sumNumbers()는 수를 더하는 일만 하게되었고, check(int number)는 수의 조건을 확인하는 일만 하게되었습니다. 예시코드가 짧아서 들여쓰기에 대한 장점이 잘 부각되지않은것 같지만 하나의 메서드는 하나의 일을 한다는 개념을 이해하셨다면 충분합니다.

 

 

 

 

 

 

버블정렬

https://ko.wikipedia.org/wiki/버블_정렬

특징

  • 안정정렬 (stable sort) 이다 
  • 추가적인 공간이 필요하지 않은 제자리 정렬( in-place sort ) 이다

 

시간복잡도

  시간복잡도
Best O(N)
Average O(N2)
Worst O(N2)

장점

  1. 구현이 매우 쉽다.
  2. 추가적인 메모리 소비가 적다.
  3. 안정정렬이다.

 

단점

  1. 정렬하는데 오랜시간이 걸린다. (시간복잡도가 높다)

 

 

 

 

정렬방법

  1. 현재 원소다음 원소를 비교한다.
  2. 현재 원소 > 다음 원소 이면 둘의 위치를 바꾼다.
  3. 다음 원소로 이동한다.

 

유튜브 영상으로도 버블정렬을 확인할 수 있습니다.

https://www.youtube.com/watch?v=Cq7SMsQBEUw

 

자바로 구현

 

기본 버블정렬

public class Bubble_Sort {

    public static void main(String[] args) {
        int[] array = {5, 3, 1, 4, 2};
        System.out.printf("초기배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
        bubble_sort(array);
        System.out.printf("정렬된배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
    }

    private static void bubble_sort(int[] array) {
        // i 는 위 그림의 Round와 같음
        for (int i = 1; i < array.length; i++) {

            // Round별 비교횟수는 Round가 증가함에 따라 횟수가 줄어듬
            for (int j = 0; j < array.length - i; j++) {

                // 현재원소가 다음원소보다 크다면 두개의 값을 변경한다.
                if (array[j] > array[j + 1]) {
                    swap(array, j);
                }
            }
        }
    }

    private static void swap(int[] array, int index) {
        int temp = array[index];
        array[index] = array[index + 1];
        array[index + 1] = temp;
    }
}

 

최적화된 버블정렬

public class Bubble_Sort {

    public static void main(String[] args) {
        int[] array = {5, 3, 1, 4, 2};
        System.out.printf("초기배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
        bubble_sort(array);
        System.out.printf("정렬된배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
    }

    private static void bubble_sort(int[] array) {
        // i == 위 그림의 Round와 같음
        for (int i = 1; i < array.length; i++) {

            boolean isSwapped = false;

            // Round별 비교횟수는 Round가 증가함에 따라 횟수가 줄어듬
            for (int j = 0; j < array.length - i; j++) {

                // 현재원소가 다음원소보다 크다면 두개의 값을 변경한다.
                if (array[j] > array[j + 1]) {
                    swap(array, j);
                    isSwapped = true;
                }
            }

            // 비교과정에서 한번도 교환이 이루어지지않았다면 반복문 종료
            if (!isSwapped) {
                break;
            }

        }
    }

    private static void swap(int[] array, int index) {
        int temp = array[index];
        array[index] = array[index + 1];
        array[index + 1] = temp;
    }
}

 

정규표현식이란?


 

정규표현식이란 문자열 데이터 중에서 원하는 조건과 일치하는 문자열을 찾기 위한것으로 미리 정의된 기호와 문자를 이용해 작성한 문자열을 말합니다.

전화번호 (010-1234-5678), 주민번호 (990101-1234567), 이메일(example@gmail.com) 과 같이 형식이 정해져있고 그 값을 검증할때 정규표현식을 사용하면 쉽게 구현할 수 있습니다.

 

 

정규표현식을 사용하면 데이터의 형식을 확인할 수 있는 장점이 있지만 식이 복잡하고 가독성이 떨어진다는 단점이 있습니다.

 

 

 

 

 

정규표현식


기호 설명 예제
. 임의의 문자 1개를 의미합니다  
[ ] 괄호안의 문자가 있는지 확인합니다. [12ab] : 1,2,a,b 중 한 문자
- 문자의 사이를 의미합니다. [0-9] : 0 ~ 9 중 한 문자
[a-z] : a ~ z 중 한 문자
[A-Z] : A ~ Z 중 한 문자
[가-힣] : 가 ~ 힣 중 한 문자
[0-9a-z-A-Z가-힣] : 전체 문자 중 한 문자
^ 문자의 시작을 의미합니다. ^a : a로 시작하는 문자
^[ab] : a,b 중 한 문자로 시작하는 문자
$ 문자의 마지막을 의미합니다. 문자 뒤에 작성합니다. a$ : a로 끝나는 문자
[a-z]$ : a ~ z 문자로 끝나는 문자
[^ ] 괄호안의 문자를 제외합니다. [^a-z] : a ~ z 까지의 문자를 제외한 모든 문자
[^123] : 1,2,3 문자를 제외한 모든 문자
| 또는 [a|b] : a 또는 b
(abc|bcd) : abc 또는 bcd
( ) 그룹을 뜻합니다. 01(0|1) : 01 뒤에 0 또는 1인 문자
{ } 개수를 의미합니다. a{3} : a가 3개인 문자 (aaa)
0{2}1 : 0이 2개이고 1이 온다 (001)

 

 

 

기호 설명 예제
{n} 앞의 표현식이 n개 있다. a{3} : a가 3개인 문자 (aaa)
[0-9]{3} : 숫자가 3개인 문자 (123, 531, 467, ... )
{n, m} 앞의 표현식이 n개 이상 m개 이하가 있다. a{1, 3} : a가 1개이상 3개이하 있다. (a, aa, aaa)
{n, } 앞의 표현식이 n개 이상있다,. a{1, } : a가 1개이상 있다, (a, aa, aaa, ... )
? 앞의 표현식이 없거나 한개만 있다. 010-? : - 가 있거나 없다 (010-, 010)
* 앞의 표현식이 없거나 여러개가 있다. 010-* : -가 여러개있거나 없다 (010, 010-, 010--, ... )
+ 앞의 표현식이 1개 이상있다 010-+ : -가 여러개있다 (010-, 010--, 010---, ... )

 

 

 

기호 설명
\d 0 ~ 9 사이의 숫자 : [0-9] 와 동일
\D 0 ~9 사이의 숫자가 아닌 문자 : [^0-9] 와 동일
\w 알파벳, 숫자, "_" 인 문자 : [0-9a-zA-Z_] 와 동일
\W 알파벳, 숫자, "_" 가 아닌 문자 : [^0-9a-zA-Z_] 와 동일
\s 공백인 문자
\S 공백이 아닌 문자
"\" 문자는 이스케이프문자를 사용해야합니다. 따라서 "\d" 정규식을 사용할때에는 "\\d" 라고 작성해야합니다.

 

 

 

 

 

어디에서 사용할까?


IDE의 자동완성을 기면 매개변수를 확인할 수 있습니다. 'String regex' 라고 되어있는 매개변수는 정규표현식을 사용할 수 있습니다.

인텔리제이의 자동완성 화면

 

 

 

 

String 정규식 문법

String 클래스에서 지원하는 정규식 메소드는 3가지가 있습니다. 

 

메서드 반환타입 설명
matches (String regex) boolean 문자열이 정규식에 매칭되는 값인지 확인
replaceAll (String regex, String repacement) String 문자열내에 정규식에 매칭되는 문자열을 replacement 문자열로 치환
split (String regex) String[] 정규식에 매칭되는 문자열을 구분자로 사용하여 분할

 

 

 

 

1. boolean matches (String regex)

String test = "1234567890";
boolean matches = test.matches("[0-9]+"); // 숫자로만 이루어진 문자열인지 확인
System.out.println(matches); // true

 

 

 

2. String replaceAll (String regex, String repacement)

String test = "test123asd,.-;";
String replaceAll = test.replaceAll("[0-9]", "A"); // 모든 숫자를 A로 치환
System.out.println(replaceAll); // testAAAasd,.-;

 

 

 

3. String[] split (String regex)

String test = "a0b1c";
String[] split = test.split("[0-9]"); // 숫자를 구분자로 사용
System.out.println(split[0]); // a
System.out.println(split[1]); // b
System.out.println(split[2]); // c

 

 

 

Pattern

 

Pattern static compile(String regex) 을 사용하여 정규식 패턴으로 변환해줍니다.

Pattern 객체로 컴파일된 정규식은 Matcher에서 사용됩니다.

String regexString = "([0-9]+)";
Pattern p = Pattern.compile(regexString); // 정규표현식을 정규식패턴으로 컴파일

 

String.matches()와 같이 Pattern.matches() 를 사용할 수 있습니다.

 

String regex = "([0-9]+)"; // 숫자로만 이루어져있는지 확인하는 정규표현식

String test= "1234567890";
boolean matches = Pattern.matches(regex, test);
System.out.println(matches); // true

String test2 = "a1234567890";
boolean matches2 = Pattern.matches(regex, test2);
System.out.println(matches2); // false

 

 

Matcher

String regexString = "[0-9]{2}"; // 숫자로 이루어진 2자리 문자열
Pattern p = Pattern.compile(regexString); // 정규식 패턴으로 변환

String test = "1a23456"; // 비교할 문자열
Matcher m = p.matcher(test); // 패턴과 비교하여 결과를 Matcher 객체로 반환

while (m.find()) { // 매칭된 결과가 있는지 확인
    System.out.println(m.group()); // 결과가 있으면 매칭된 부분 반환
    // 결과 : 23
    // 결과 : 45
}

 

반환타입 메서드 설명
find() boolean 패턴과 일치하는 문자열이 있을경우 true, 없으면 false
group() String 매칭된 문자열을 반환
group(int group) String 매칭된 여러 그룹중 int group 번째 문자열 반환
groupCount() int 매칭된 그룹의 개수 반환
matches() boolean 전체 문자열과 일치할경우 true, 일치하지 않으면 false
start() int 매칭된 문자열의 시작 인덱스 반환
end() int 매칭된 문자열의 끝 인덱스 반환

 

start() 와 end()

String regexString = "하세";
Pattern p = Pattern.compile(regexString);

String test = "안녕하세요";
Matcher m = p.matcher(test);
System.out.println(m.find()); // true
System.out.println("문자열 위치 : " + m.start() + " ~ " + m.end()); // 문자열 위치 : 2 ~ 4

 

 

* 눈치 채신분들도 계시겠지만 매칭된 문자열을 반환하기전에 find() 메서드를 사용하는걸 볼 수 있습니다. 그럼 만약 find() 메서드를 사용하지 않으면 어떻게 될까요?

 

예외발생

 

매칭결과를 얻기전에 항상 find() 메서드가 선행되어야합니다. 그렇지 않으면 문자열을 반환하는 시점에 IllegalStateException 예외가 발생합니다.

 

잘못된것이 있으면 언제든 지적해주세요!

+ Recent posts