오늘은 정규 표현식에 대해서 알아보고! 추가적으로 알아본것들에 대해서 같이 작성하면서 마무리를 해보겠습니다.

Regular Expression은, 특정한 규칙을 가진 문자열의 집합을 표현하는데 쓰이는 형식 언어입니다. 이러한 표현식을 사용하여, 특정한 문자를 찾을 수 도 있고, 찾은 문자열을 다른 문자열로 고칠 수도 있어 아주 유용하게 사용할 수 있습니다.
이러한 정규 표현식을 통해서 다양한 작업을 할 수 있으나, 그중에서 몇가지만 선별하여 보여드리겠습니다.
const waluigi = /wa+(ha+)+/;
waluigi.test('waha'); // returns true
waluigi.test('waaaahaaaaha'); // returns true
waluigi.test('waahahaahahaa'); // returns true
위는 현재 자바스크립트의 예시를 가져왔습니다. 하지만, 전체적인 흐름은 같기 때문에 설명드리겠습니다. 위에서 보시는것 처럼 waluigi라는 변수에 내가 확인할 표현식을 정의하였고, 아래의 test 메서드를 통하여 확인을하는 모습을 볼 수 있습니다.
const carat = /^\d/;
carat.test('5 time 5 is 25'); // returns true
carat.test('Five time five is 25'); // returns false
const dollar = /\d$/;
dollar.test('five times five is 25') // returns true
dollar.test('five times five is twenty-five') // returns false
const caratDollar = /^\d.+\d$/;
caratDollar.test('5 times 5 is 25') // returns true
caratDollar.test('5 times 5 is twenty-five') // returns false
caratDollar.test('Five times 5 is 25') // returns false
caratDollar.test('Five times 5 is twenty-five') // returns false
위의 Boundary는 보는 것처럼 테스트로 입력되는 문자열의 time과 ^, 이 두글자를 매핑하여 포함이 되어 있는지 아닌지를 파악하는 코드임을 알 수 있습니다.
const replaceAllExp = /(cat|dog|fish)/g;
const replaceAllString = 'cat dog fish'
replaceAllString.replace(replaceAllExp, 'turkey'); // returns 'turkey turkey turkey'
const namesExp = /(\w+), (\w+)/g
const names = 'Potter, Harry, Weasley, Ronald, Granger, Hermione';
names.replace(namesExp, "$2 $1"); // returns "Harry Potter, Ronald Weasley, Hermione Granger"
const funcExp = /\b(jfk|fdr)\b/g
const presidents = "I prefer jfk to fdr";
presidents.replace(funcExp, str => str.toUpperCase()); // returns "I prefer JFK to FDR"
위의 String methods는 Regular Expression에서 제일 많이 사용되는 표현식으로, 이러한 방법이 있다는 것을 알고 있으면 될 것 같습니다! 저도 Calculator를 제작을 할 때 위의 정규표현식을 사용해서 연산자의 입력을 확인 하는 등의 작업을 진행했습니다.
private static final String MODE_REG = "\\b(circle|rect)\\b";
private static final String PROCESS_REG = "\\b(exit|remove|inquiry|change)\\b";
private static final String OPERATION_REG = "[+\\-*/%]";
위 처럼 선언을 해두고, Pattern.matches()라는 메서드와 함께 사용을 합니다. 그러면 바로 다음으로 넘어가겠습니다
자바의 Pattern 클래스는 정규 표현식이 컴파일된 클래스이다. 라고 생각하시면 될것 같습니다.
Pattern p = Patten.compile("a...b");
Matcher m = p.matcher("aaab");
boolean b = m.matches(); // true
위는 Java reference page에서 확인 할 수 있는 내용으로, Pattern.compile()이라는 것으로 내가 적용할 패턴을 즉, 정규 표현식을 입히고 Matcher이라는 클래스를 불러와서 확인할 문자열을 대입시킵니다. 그 결과를 matches()라는 메서드를 통해 boolean 타입으로 확인 할 수 있습니다.
이렇게 비교를 하여 결과를 도출해 주는 matches() 메서드를 조금 더 알아보겠습니다.
보통은 컴파일과 비교를 한 번에 해준는 것이 matches 메서드입니다. 그렇게 하면 코드를 간단하게 줄일 수 있게 됩니다. 가령 제가 사용한 예시는 다음과 같습니다.
if(!Pattern.matches(PROCESS_REG, process)){
throw new CustomException("This is not a valid process");
}
이렇게 특정 문자를 포함하는지 검사하는 또 다른 메서드들은 .contains()가 있습니다. 이러한 차이점은 contains() 같은 경우 특정 문자열을 받아서 확인을 하는 반면에 .matches()의 경우는 정규 표현식을 인자를 받아 동일한 패턴의 문자열인지를 확인하는 메서드 입니다.
Calculator 프로그래밍을 하던 도중, method에서 return this 를 반환하는 것을 많이 봤었습니다. 이러한 구조를 사용하는 이유에 대해서 같이 포스팅을 하겠습니다!
클래스 내부에서 this 자체로 쓰이는 경우, 아직 객체로 만들어지기 전에 참조값을 가질 수 있나 싶지만, 객체가 메모리를 할당 받는 순간에 그 객체 변수의 참조값을 의미하는 것입니다. 그러면 이러한 것을 static method 에서도 사용이 가능한지도 같이 알아봅시다.
사용할 수 없습니다.
Class Player {
Player setName(String name){
this.name = name;
return this;
}
// or
void setName(String name){
this.name = name;
}
}
위의 두 코드는 같은 동작을 수행하는데 return 값을 두는 이유는 그냥 단순하게 형식을 정해서라고 생각할 수 있을것 같습니다. 그냥 void로 만들면 리턴을 굳이 안해도 됩니다. 만약에 두 메서드를 이용해서 이름을 불러오는 동작을 수행하려고 하면 아래처럼 할 수 있을 것 같습니다.
player.setName().getName();
player.setName();
player.getName();
보시는 것 처럼 엄청 간단하게 한 줄로 표현 할 수도 있고, 상황에 따라서는 더 복잡한 작업으로 작용할 수 있습니다. 상황에 맞게 선택해서 구조를 정하는 것이 좋을 것 같습니다.
Generic이라는 말을 그대로 직역을 해보자면, 일반적인 이라는 뜻을 가지고 있습니다. 즉, 데이터 형식에 얽메이지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법입니다.
📌 제네릭을 그러면 왜 사용을 할까?
제가 실습을 했던 계산기의 예제를 들어서 설명을 해보겠습니다.
조건에서 제시하는 것은 mode가 입력이 되면, 그에 따른 인스턴스 생성을 다르게 하는 방식입니다. 이러한 조건이 필요할 때 Generic을 사용합니다.
위 처럼 제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미합니다. 한마디로 특정 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반적인 타입이라는 것입니다.
사용방법
Type Explanation <T>Type <E>Element <K>Key <V>Value <N>Number
위의 사용방법을 보면 한글자들로만 적혀있는 것을 확인 할 수 있습니다. 하지만 굳이 한글자로만 적을 필요는 없습니다만, 이렇게 표시하는 것이 가장 편한 방법이기에 선택한 것임을 알아두면 좋을 것 같습니다.
public class ClassName<T>{...}
public interface InterfaceName<T>{...}
기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와 같이 선언합니다. 여기서 T 타입은 해당 블럭 안에서까지 유효합니다.
public class ClassName<T, K>{...}
public interface InterfaceName<T, K>{...}
// HashMa의 경우 아래와 같이 선언되어 있을 수 있습니다.
public class HashMap <K,V>{...}
여기서 단 주의해야 할 점은 타입으로 오는 것들은 무조건 Reference Type 만이 가능합니다.
public class ClassName<T>{....}
public class Student{...}
public class Main{
public static void main(String args[]){
ClassName<Student> a = new ClassName<Student>();
}
}
이제 본격적으로 클래스에 구현하는 방법을 뜯어 보겠습니다.
class ClassName<E>{
private E element;
void set(E element){
this.element = element;
}
E get(){
return element;
}
}
class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<String>();
ClassName<Integer> b = new ClassName<Integer>();
a.set("10");
b.set(10);
System.out.println("a data : " + a.get());
// 반환된 변수의 타입 출력
System.out.println("a E Type : " + a.get().getClass().getName());
System.out.println();
System.out.println("b data : " + b.get());
// 반환된 변수의 타입 출력
System.out.println("b E Type : " + b.get().getClass().getName());
}
}
이렇게 한 클래스에 다른 변수 두개를 정의했습니다. 여기서 get을 하게 되면 a는 String으로 변환되고, b는 Integer로 변환되게 됩니다. 여기서 두개의 타입을 동시에 쓰는 것은 위에 방법을 활용해서 진행 할 수 있습니다.
위의 선언 방식은 클래스 이름 옆에 <타입>을 붙여서 사용했습니다.메서드에서는 다른 방식으로 사용할 수 있습니다. 방법은 아래와 같습니다.
public <T> T genericMethod(T o){
...
}
[접근 제어자] <제네릭 타입> [반환 타입] [메서드 명]([제네릭 타입] [파라미터]){
}
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메소드
this.element = element;
}
E get() { // 제네릭 타입 반환 메소드
return element;
}
<T> T genericMethod(T o) { // 제네릭 메소드
return o;
}
}
public class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<String>();
ClassName<Integer> b = new ClassName<Integer>();
a.set("10");
b.set(10);
System.out.println("a data : " + a.get());
// 반환된 변수의 타입 출력
System.out.println("a E Type : " + a.get().getClass().getName());
System.out.println();
System.out.println("b data : " + b.get());
// 반환된 변수의 타입 출력
System.out.println("b E Type : " + b.get().getClass().getName());
System.out.println();
// 제네릭 메소드 Integer
System.out.println("<T> returnType : " + a.genericMethod(3).getClass().getName());
// 제네릭 메소드 String
System.out.println("<T> returnType : " + a.genericMethod("ABCD").getClass().getName());
// 제네릭 메소드 ClassName b
System.out.println("<T> returnType : " + a.genericMethod(b).getClass().getName());
}
}
즉, 클래스에서 지정한 제네릭 유형과 별도로 메서드에서 독립적으로 제네릭 유형을 선언하여 쓸 수 있습니다. 이 같은 방식은 정적 메서드로 선언할 때 사용이 됩니다.
정적 메서드라는 것은 static으로 선언된 메서드로, 객체를 다시 생성하여 접근하는 것이 아닌 이미 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 쓸 수 있습니다. 그러면, static method는 객체가 생성되기 전에 이미 메모리에 올라가 있는데 타입을 어디서 얻어 올 수 있을까?
class ClassName<E> {
/*
* 클래스와 같은 E 타입이더라도
* static 메소드는 객체가 생성되기 이전 시점에
* 메모리에 먼저 올라가기 때문에
* E 유형을 클래스로부터 얻어올 방법이 없다.
*/
static E genericMethod(E o) { // error!
return o;
}
}
class Main {
public static void main(String[] args) {
// ClassName 객체가 생성되기 전에 접근할 수 있으나 유형을 지정할 방법이 없어 에러남
ClassName.getnerMethod(3);
}
}
위의 이유 때문에 제네릭이 사용되는 메서드를 정적 매서드로 두고 싶은 경우 제네릭 클래스와 별도로 독립적인 제네릭이 사용되어야 합니다.
// 제네릭 클래스
class ClassName<E> {
private E element; // 제네릭 타입 변수
void set(E element) { // 제네릭 파라미터 메소드
this.element = element;
}
E get() { // 제네릭 타입 반환 메소드
return element;
}
// 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
static <E> E genericMethod1(E o) { // 제네릭 메소드
return o;
}
static <T> T genericMethod2(T o) { // 제네릭 메소드
return o;
}
}
public class Main {
public static void main(String[] args) {
ClassName<String> a = new ClassName<String>();
ClassName<Integer> b = new ClassName<Integer>();
a.set("10");
b.set(10);
System.out.println("a data : " + a.get());
// 반환된 변수의 타입 출력
System.out.println("a E Type : " + a.get().getClass().getName());
System.out.println();
System.out.println("b data : " + b.get());
// 반환된 변수의 타입 출력
System.out.println("b E Type : " + b.get().getClass().getName());
System.out.println();
// 제네릭 메소드1 Integer
System.out.println("<E> returnType : " + ClassName.genericMethod1(3).getClass().getName());
// 제네릭 메소드1 String
System.out.println("<E> returnType : " + ClassName.genericMethod1("ABCD").getClass().getName());
// 제네릭 메소드2 ClassName a
System.out.println("<T> returnType : " + ClassName.genericMethod1(a).getClass().getName());
// 제네릭 메소드2 Double
System.out.println("<T> returnType : " + ClassName.genericMethod1(3.0).getClass().getName());
}
}
위에는 가장 일반적인 제네릭들의 예시입니다. 제네릭은 참조 타입 모두가 될 수 있는데, 여기서 특정 범위 내로 좁혀서 제한하고 싶다면 어떻게 해야할까요?
이 때 필요한 것이 바로 extends와 super, 그리고 ? 입니다. 여기서 와일드 카드는 물음표를 의미합니다. 즉, 알 수 없는 타입이라는 의미를 함유하고 있습니다.
<K extends T> // T와 T의 자손 타입만 가능합니다.(K는 들어오는 타입으로 지정 됩니다.)
<K super T> // T와 T의 부모 타입만 가능합니다.(K는 들어오는 타입으로 지정 됩니다.)
<? extends T> // T와 T의 자손 타입만 가능합니다.
<? super T> // T와 T의 부모 타입만 가능합니다.
<?> // 모든 타입이 가능합니다. <? extends Object>와 같은 의미입니다.
위의 코드를 정리하면 아래와 같습니다.
1. extends T : 상한 경계
2. ? super T : 하한 경계
여기서 가질 수 있는 궁금증이 있을 수 있습니다.
❓ 그냥 와일드 카드 쓰면 되는거 아닌가?
라는 의문을 물론 가질 수 있습니다. 위의 질문에 응답을 하기 위해서 제가 푼 Calculator를 예시로 들어 설명을 해보겠습니다.

현재 위의 다이어그램을 확인하면, 다음과 같습니다. 현재 CalculatorApp이라는 것을 전반적인 프로그램을 실행하고 있습니다. 그런데 입력되는 값이 전부 유효한지 확인하기 위해서, Parser라는 클래스를 통해서 확인하고 각 클래스를 호출하게 됩니다.
만약, 코드의 설계를 Calculator라는 추상 클래스를 통해서 각 Rect와 Circle에 컴포지션 혹은 상속하여 보면 이런 식으로 작성할 수 있습니다.
// Calculator를 상속 받는 타입만이 올 수 있습니다.
// 객체 혹은 메서드를 호출 할 경우는 K는 지정된 타입으로 변환됩니다.
<K extends Calculator>
// 위처럼 상속 받는 타입만 올 수 있지만, 따로 호출 할때 타입을 지정할 수 없습니다.
// 타입 참조가 불가능합니다.
<? extends T>
여기서, 잠시 extends와 super에 대해서 간략하게 설명을 드리겠습니다. 제가 올린 다이그램과 같은 상속관계를 가진 클래스들이 있다고 가정하겠습니다.
<T extends Rect> // Rect만
<T extends Circle> // Circle만
<T extends Calculator> // Rect, Circle, Calculator
<T extends CalculatorApp> // Parser, Rect, Circle, Calculator, CalculatorApp
즉, extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해지게 됩니다.
<T super Rect> // Rect만
<T super Circle> // Circle만
<T super Calculator> // Parser, CalculatorApp, Calculator
<T super CalculatorApp> // 본인만
위의 extends와 반대로 super 타입으로 부터 시작 선이 그어지는 느낌이라고 생각하시면 됩니다. 이렇게 regrex(Regular Expression), Generic, Return this에 대해서 알아봤습니다.
References
1. 정규 표현식
2. Regular expression examples
3. Return this explanation
4. Generic comphrehencible texts