우리는 0, 1, 2, 5, 10 에대한 case를 작성했는데 바이트 코드를 보면 TABLESWITCH 안에 0 - 10까지의 데이터가 들어가있습니다. 0, 1, 2, 5, 10을 제외한 나머지 숫자들은 switch 종료 라인인 L5 를 가리키고 있습니다.
TABLESWITCH는 전체 범위를 인덱싱 하기 때문인데요. 이 과정에서 사용되지 않은 빈 공간까지 차지하게 되어버렸습니다.
LOOKUPSWITCH와 TABLESWITCH는 컴파일과정에서 효율성을 따져서 자동으로 이 방식이 선택됩니다.
만약 인덱싱과정이 사용된다면 더더욱 switch문을 써야겠죠?
그래서 뭐가 더 좋을까?
코드를 작성할 때, 성능도 중요하지만 가독성에 대한 것도 고려해야합니다. JAVA 17부터 switch 문이 업데이트되면서 기존의 if-else 보다 가독성이 좋아졌습니다.
// switch 문
switch (i) {
case 1:
method1();
break;
case 3:
method2();
break;
default:
method3();
break;
}
// JAVA 17 에서의 switch 문
switch (i) {
case 1 -> method1();
case 3 -> method2();
default -> method3();
}
switch (i) { // 다수 조건식
case 1,2 -> method1();
case 3 -> method2();
default -> method3();
}
// JAVA 21 에서의 switch 문
switch (animal) { // 타입 캐스팅
case Cat c -> c.sound();
case Dog d -> d.sound();
case Cow c -> c.sound();
default -> "Unknown";
};
switch (animal) { // Null 처리
case Cat c -> c.sound();
case Dog d -> d.sound();
case Cow c -> c.sound();
case null -> "Null";
default -> "Unknown";
};
따라서, 가독성에 중점을 두거나 조건이 많을때는 switch 문을 사용하는것도 고려해볼만 합니다.
String의 불변특성때문에 문자열 더하기의 연산은 불필요한 데이터를 저장하게되며, hashCode의 캐싱에도 불필요한 데이터가 저장된다는 문제가 있습니다. 따라서 String의 문자열 더하기는 StringBuilder 나 StringBuffer를 사용하도록 해야합니다.
그러면 StringBuilder와 StringBuffer는 무슨 차이가 있을까요?
StringBuilder vs StringBuffer
Builder와 Buffer는 같은 추상클래스를 상속받고 있고, 그 구조가 완벽하게 동일합니다. 그 차이점은 구현체에서 찾을 수 있습니다.
StringBuffer는 동기화를 지원하고 (Thread-Safe),
StringBuilder는 동기화를 지원하지 않습니다.
StringBuffer 대부분의 메소드에는 synchronized 키워드가 붙어있습니다.
언제 어떻게 사용하면 될까?
StringBuffer
동기화를 지원하기때문에 멀티스레드 환경에서 안전하게 동작합니다. 동시성 문제를 고려해야한다면 StringBuffer를 사용해야합니다.
StringBuilder
StringBuilder는 StringBuffer 보다 일반적으로 성능상 빠릅니다. 동기화 처리가 없기 때문입니다. 따라서 단일스레드 환경에서는 불필요한 동기화 처리를 하지 않는 StringBuilder를 사용해야합니다.
사용 시 주의점
1. capacity
StringBuilder, StringBuffer 둘다 용량증가정책은 동일하게 설정되어있습니다. 이 capacity는 문자열의 수용량입니다. 기본 capacity는 16입니다.
String은 Heap 영역에서 String Constant Pool 에서 관리되고있습니다.
""(큰따옴표)로 생성된 객체는 String Constant Pool 에서 관리되어 다음 동일한 값이 할당된다면 이전에 있던 참조값을 반환해줍니다. ( == true)
new 연산자를 사용한다면 String Constant Pool에 같은 값이 존재하더라도 별도의 주소를 가리키게됩니다. ( == false)
만약 위 그림에서 str2 = "apple" 이었다가 "banana"로 값이 변경된다면 어떻게 될까요?
변수 str2의 값이 "banana"로 변경된다면 String Constant Pool에서 값이 "banana"인 주소를 찾고 없다면 새로 생성하여 그 주소를 반환해줍니다. 이 과정에서 이전의 "apple" 데이터는 변경되지 않습니다.
불변인 이유
String Constant Pool에서 String 을 관리한다는것은 동일한 데이터에 대해 메모리주소를 공유함으로써 Heap 영역의 메모리가 절약한다는 뜻이됩니다. 이 과정에서 String 이 불변성을 유지해야지 메모리 주소를 공유하는 것이 가능합니다. 만약 String이 불변객체가 아니라면 변수 str1과 str2가 "apple" 의 메모리주소를 공유하고있다고했을때 str2에서 값이 변경되는 순간 str1도 값이 변경되기 때문에 불변성을 가져야합니다.
String 객체에 대해 hashCode를 미리 계산하지 않아 메모리를 절약하고, 필요할 때만 계산하고, 계산된 후에는 재사용하여 성능을 향상 시킵니다. hashCode를 재사용한다는 것은 String의 불변성이 보장되어야 가능합니다. hashCode의 캐싱은 HashMap, HashSet과 같은 hash를 사용하는 연산에서 효과적입니다. 일반 객체는 사용할때마다 hashCode를 계산하지만 String은 캐싱된 hashCode를 사용하기 때문에 더욱 빠르게 연산하는 것이 가능합니다.
Sring은 불변이기 때문에 Thread-Safe합니다. 값이 변경되지않는 것이 보장된다면 동기화 문제가 발생할 수가 없습니다.
String이 불변이 아니라면 보안에 문제가 생길 수 있습니다. 데이터베이스의 username, password이나 host, port가 String값으로 전달된다고 했을때, 이 값이 불변이 아니라면 공격으로부터 값이 변경될 수 있기 때문입니다.
이상 String의 불변성에 대해서 알아보았습니다. 문자열의 기본타입이 존재하지않는 이유가 String의 불변성과 관련이 있다는 것을 알아가는 시간이었습니다.
이전 글에서 만들었던 금지어 검사기를 Maven Central Repository에 등록해보겠습니다.
프로젝트 라이브러리화
@Configuration
@ComponentScan("ban.inspector")
public class WordInspectorAutoConfig {
}
이전 프로젝트를 그대로 배포해버리면 @Component 클래스들이 스캔범위 밖으로 벗어나기때문에 @ComponentScan 으로 해당 패키지를 설정해줍니다.
resources 하위에 META-INF 하위에 spring 폴더를 만듭니다.
org.springframework.boot.autoconfigure.AutoConfiguration.imports 라는 이름으로 file을 만듭니다.
위에 파일 안에 @ComponentScan 이 붙은 클래스의 패키지주소를 작성합니다.
org.springframework.boot.autoconfigure.AutoConfiguration.imports 내부에 작성된 config 클래스의 패키지주소
이렇게 설정하면 외부라이버리를 Component 로 손 쉽게 사용할 수 있습니다.
Maven Central Repository 등록하기
2024년 3월 12일부터issues.sonatype.org는 폐쇄되어 모든 등록은 중앙 포털을 통해 이루어지도록 변경되었습니다. 기존 Nexus repository(OSSRH)로 올리는 방식을 사용하던 사용자는 기존 방식을 그대로 사용할 수 있지만, 신규 사용자라면 Maven Central을 통해서 라이브러리를 배포해야 합니다. 따라서 해당 게시글은2024년 3월 12일이후 적용되는 중앙포털을 통해 배포하는 방식입니다.
Maven Central Repository
Maven Central Repository는 Maven 프로젝트를 위한 공개 저장소입니다.
우리가 흔히 Maven Repository 에서 라이브러리를 찾아서 사용하는데 Maven Repository 는 라이브러리를 검색하는 곳이고, Maven Central Repository는 실제 파일을 등록하는 곳이라고 생각하시면 됩니다.
검사할 문자열을 inspect() 에 넣으면 금지된 단어의 문자열과 시작인덱스(includeIndex), 마지막인덱스(excludeIndex) 가 포함된 List 데이터가 반환 되어야 한다.검사할 문자열을 inspect() 에 넣으면 금지된 단어의 문자열과 시작인덱스(includeIndex), 마지막인덱스(excludeIndex) 가 포함된 List 데이터가 반환 되어야 한다. 마지막인덱스는 띄어쓰기가 포함한 인덱스이다.
예외단어에 포함된 문자열은 금지어에서 제외한다.
검사할 문자열에서 금지된 단어를 블라인드하려면 mask() 메소드를 사용한다. 두번째인자로 "?" 대신 변경할 문자열을 넣으면 해당 문자로 변경되어 반환한다.
구현
1. Inspector
public interface Inspector {
List<Word> inspect(String word);
default String mask(String word) {
return mask(word, "?");
}
default String mask(String word, String replace) {
StringBuilder sb = new StringBuilder(word);
List<Word> data = inspect(word);
for (int i = data.size() - 1; i >= 0; i--) {
sb.replace(data.get(i).startIndex(), data.get(i).endIndex(), replace);
}
return sb.toString();
}
}
가장 먼저 Inspector 인터페이스를 만들어줍니다.
메소드
설명
inspect(String word) : List<Word>
검사할 문자열이 인자로 들어오면 금지어 목록을 반환합니다.
default mask(String word) : String
금지어들을 replace 할 기본 문자열을 "?" 로 정의해 반환합니다.
default mask(String word, String replace) : String
금지어들을 replace 문자열로 변환합니다.
※ 왜 StringBuilder를 썼을까?
replaceAll 보다 StringBuilder를 사용하는게 더 빠릅니다.
금지단어가 여러개일 경우 for 문을 사용해 순환할 때 문자열의 길이가 변경되어 다음 순환 시 StringIndexOutOfBoundsException 가 발생할 수 있습니다.
replaceAll에는 예외를 둘 수가 없습니다. 2번을 보완해 기존 데이터를 살려두고 replaceAll을 하는 경우에는 금지단어가 제외되지만 같은 단어로써 예외단어까지 영향을 미칠 수 있습니다.
// 금지어 : ["사과", "바나나"]
// 예외단어 : ["사과주스"]
String word = "사 과랑바나나";
List<Word> banWords = [
(Word: word="사과", startIndex=0, endIndex=3),
(Word: word="바나나", startIndex=4, endIndex=7)
];
// 1.
for (Word data : banWords) {
word = word.replaceAll(word.substring(data.startIndex, data.endIndex), replace);
// StringIndexOutOfBoundsException 발생
}
// 2.
String word = "사과랑 사과주스";
String replace = "?";
String temp = word;
for (Word data : banWords) {
word = word.replaceAll(temp.substring(data.startIndex, data.endIndex), replace);
}
// 예상한 결과 : ?랑 사과주스
// 실제 결과 : ?랑 ?주스
2. Inspector 구현체
@Component
public class InspectorImpl implements Inspector {
private final BanWordUtil banWordUtil;
private final ExceptWordUtil exceptWordUtil;
@Autowired
public InspectorImpl(InnerInspectConfig config) {
banWordUtil = config.getBanWordUtil();
exceptWordUtil = config.getExceptWordUtil();
}
private List<Word> executeBanWord(String word) {
return banWordUtil.filter(word);
}
private List<Word> executeExceptWord(String word, List<Word> beforeWords) {
return exceptWordUtil.filter(word, beforeWords);
}
@Override
public List<Word> inspect(String word) {
return executeExceptWord(word, executeBanWord(word));
}
}
mask() 메소드는 default 에 정의해놓았기때문에 Override를 하지 않았습니다.
executeExceptWord(word, executeBanWord(word));
메소드는 예외단어처리(검사할문자열, 금지어단어목록) 으로 보시면됩니다.
3. Config
@Component
public class InnerInspectConfig {
private final WordFactoryBuilder<BanWordUtil> banWordFactory;
private final WordFactoryBuilder<ExceptWordUtil> exceptWordFactory;
private final WordLoader wordLoader;
private InspectConfig inspectConfig;
@Autowired
public InnerInspectConfig(WordFactoryBuilder<BanWordUtil> banWordFactory, WordFactoryBuilder<ExceptWordUtil> exceptWordFactory, WordLoader wordLoader) {
this.banWordFactory = banWordFactory;
this.exceptWordFactory = exceptWordFactory;
this.wordLoader = wordLoader;
}
@Autowired(required = false)
public void setInspectConfig(InspectConfig inspectConfig) {
this.inspectConfig = inspectConfig;
}
@PostConstruct
private void onApplicationReady() {
if (inspectConfig != null) {
inspectConfig.addBanWords(banWordFactory);
inspectConfig.addExceptWords(exceptWordFactory);
}
banWordFactory.add(wordLoader.readBanWords());
exceptWordFactory.add(wordLoader.readExceptWords());
}
public BanWordUtil getBanWordUtil() {
return banWordFactory.build();
}
public ExceptWordUtil getExceptWordUtil() {
return exceptWordFactory.build();
}
}
내부 설정을 담당하는 클래스입니다.
이곳에서 BanWordUtil과 ExceptWordUtil을 build해서 Inspector에 전달해주는 역할을 하게 만들었습니다.
메소드
설명
setInspectConfig
사용자로부터 금지어 및 예외단어를 추가할 수 있게 하기 위해 setter 주입으로 구현했습니다. Autowired(required = false)를 함으로써 사용자가 Config를 구현하지 않더라도 문제가 없도록 구현하기 위해 required = false를 했습니다.
onApplicationReady
의존성 주입이 완료되고(@PostConstruct) 사용자가 추가한 데이터(inspectConfig)와 기본데이터인 json 데이터(wordLoader.read~)를 각 factory에 추가합니다.
getBanWordUtil getExceptWordUtil
build 된 WordUtil을 Inspector에서 get 하는 메소드입니다.
4. WordLoader
public interface WordLoader {
List<String> readBanWords();
List<String> readExceptWords();
}
@Component
public class WordLoaderImpl implements WordLoader {
private final Log logger = LogFactory.getLog(WordLoaderImpl.class);
@Override
public List<String> readBanWords() {
return read("static/BanWords.json");
}
@Override
public List<String> readExceptWords() {
return read("static/ExceptWords.json");
}
private List<String> read(String path) {
try {
return new ObjectMapper().readValue(new ClassPathResource(path).getInputStream(), new TypeReference<>() {});
} catch (IOException e) {
logger.error(e);
return Collections.emptyList();
}
}
}
Json 데이터를 읽어오는 클래스입니다.
5. WordFactory
public interface WordFactory {
WordFactory add(List<String> words);
}
public interface WordFactoryBuilder<T extends AbstractWordUtil> extends WordFactory {
T build();
}
WordFactory 인터페이스는 사용자로부터 금지어, 예외단어를 주입받기 위해 사용하는 클래스이고,
WordFactoryBuilder<T extends AbstractWordUtil> 는 Config에서 WordUtil을 build 하기 위해 사용되는 인터페이스입니다.
※WordFactoryBuilder 를 도입한 이유
factory와 util 간의 메소드 분리
사용자로부터 build 메소드가 노출되지 않아야함
BanWordFactory, ExceptWordFactory에 build는 무조건 실행되어야 하기때문에 interface로 강제하기위함
구현체
public abstract class AbstractWordFactory<T extends AbstractWordUtil> implements WordFactoryBuilder<T> {
private final T wordUtil;
private final Set<String> distinctWordSet = new HashSet<>();
public AbstractWordFactory(T wordUtil) {
this.wordUtil = wordUtil;
}
@Override
public WordFactory add(List<String> words) {
distinctWordSet.addAll(words);
return this;
}
@Override
public T build() {
distinctWordSet.forEach(wordUtil::addWord);
wordUtil.build();
return wordUtil;
}
}
@Component
public class BanWordFactoryImpl extends AbstractWordFactory<BanWordUtil> {
@Autowired
public BanWordFactoryImpl(BanWordUtil banWordUtil) {
super(banWordUtil);
}
}
@Component
public class ExceptWordFactoryImpl extends AbstractWordFactory<ExceptWordUtil> {
public ExceptWordFactoryImpl(ExceptWordUtil exceptWordUtil) {
super(exceptWordUtil);
}
}
add된 words 는 Set으로 중복제거를 거쳐 마지막에 build로 wordUtil을 반환시킨다. Impl 인 구현체들은 생성자 주입을 담당하는 역할만 수행합니다.
6. AbstractWordUtil
public abstract class AbstractWordUtil {
protected final WordUtil wordUtil;
public AbstractWordUtil(WordUtil wordUtil) {
this.wordUtil = wordUtil;
}
public void addWord(String word) {
wordUtil.addWord(word);
}
public void build() {
wordUtil.build();
}
}
@Component
public class BanWordUtil extends AbstractWordUtil {
public BanWordUtil(@Qualifier("ban") WordUtil wordUtil) {
super(wordUtil);
}
public final List<Word> filter(String word) {
return wordUtil.search(word);
}
}
@Component
public class ExceptWordUtil extends AbstractWordUtil {
public ExceptWordUtil(@Qualifier("except") WordUtil wordUtil) {
super(wordUtil);
}
public final List<Word> filter(String newWord, List<Word> banWords) {
return (banWords.isEmpty()) ? List.of() : expectFilter(newWord, banWords);
}
private List<Word> expectFilter(String newWord, List<Word> banWords) {
List<Word> exceptWords = wordUtil.search(newWord);
if (exceptWords.isEmpty()) return banWords;
List<Word> newWords = new ArrayList<>();
a:for (Word banWord : banWords) {
for (Word exceptWord : exceptWords) {
if (banWord.isInclude(exceptWord)) continue a;
}
newWords.add(banWord);
}
return newWords;
}
}
자료구조를 제어하는 클래스입니다. BanWordUtil과 ExceptWordUtil에 반드시 존재해야하는 addWord(), build() 메소드를 추상클래스로 정의했습니다. 자료구조는 WordUtil 자리에 들어갈겁니다.
가장 중요한 부분입니다. 바로 문자열 검사에 대한 알고리즘이 들어가는 부분인데요. 처음에 이 부분을 직접 구현해서 사용했었는데, 위의 우아한기술블로그에서 소개한 아호코라식 알고리즘 을 기반으로 커스텀해서 띄어쓰기를 허용한 데이터를 반환하게 구성했습니다.
@Component
public class AhoCorasickWordUtil implements WordUtil {
private final TrieNode root;
public AhoCorasickWordUtil() {
this.root = new TrieNode();
}
static private class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
TrieNode failureLink = null;
Set<String> output = new HashSet<>();
}
@Override
public void addWord(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
node = node.children.computeIfAbsent(word.charAt(i), k -> new TrieNode());
}
node.output.add(word);
}
@Override
public void build() {
Queue<TrieNode> queue = new LinkedList<>();
root.failureLink = root;
for (TrieNode node : root.children.values()) {
node.failureLink = root;
queue.add(node);
}
while (!queue.isEmpty()) {
TrieNode current = queue.poll();
for (Map.Entry<Character, TrieNode> entry : current.children.entrySet()) {
char c = entry.getKey();
TrieNode child = entry.getValue();
TrieNode failure = current.failureLink;
while (failure != root && !failure.children.containsKey(c)) {
failure = failure.failureLink;
}
if (failure.children.containsKey(c) && failure.children.get(c) != child) {
child.failureLink = failure.children.get(c);
} else {
child.failureLink = root;
}
child.output.addAll(child.failureLink.output);
queue.add(child);
}
}
}
@Override
public List<Word> search(String word) {
List<Word> result = new ArrayList<>();
if (word == null || word.isEmpty()) return result;
TrieNode node = root;
int realIndex = 0;
int nonSpaceIndex = 0;
Map<Integer, Integer> startIndices = new HashMap<>();
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
if (Character.isWhitespace(c)) {
realIndex++;
continue;
}
startIndices.put(nonSpaceIndex, realIndex);
while (node != root && !node.children.containsKey(c)) {
node = node.failureLink;
}
if (node.children.containsKey(c)) {
node = node.children.get(c);
}
for (String pattern : node.output) {
int start = startIndices.getOrDefault(nonSpaceIndex - (pattern.length() - 1), -1);
int end = realIndex + 1;
if (start != -1) {
result.add(new Word(pattern, start, end));
}
}
nonSpaceIndex++;
realIndex++;
}
return result;
}
}
8. Word
public record Word(String word, int startIndex, int endIndex) {
public boolean isInclude(Word word) {
return word.startIndex <= startIndex && word.endIndex >= endIndex;
}
}
@Configuration
public class TestConfig implements InspectConfig {
@Override
public void addBanWords(WordFactory factory) {
factory
.add(List.of("사과", "바나나", "오렌지", "수박"))
.add(List.of("오이", "감자", "고구마"));
}
@Override
public void addExceptWords(WordFactory factory) {
factory.add(List.of("사과주스", "호박고구마"));
}
}
json 기본 데이터 외에 사용자가 금지어와 예외단어를 추가할 수 있습니다.
잘 동작합니다.
마무리하면서 느낀점
이번 금지어 검사기는 즉흥적으로 시작했던터라 생각보다 시간이 좀 걸렸던 것같습니다. 자그마한 프로젝트를 하면서 배운게 몇가지가 있는습니다.
JAVA 문자열 처리 앞서 말한대로 알고리즘을 사용하기 전에 직접 만들어보며 문자열에 대한 이해도가 높아졌고, 아호코라식 알고리즘을 알게 되고, 사용해본게 가장 유용했습니다.
기능분리 위의 자료구조를 보면 - 설정파트를 담당하는 InspectConfig, InnerInspectConfig - JSON을 불러오는 WordLoader - 설정파트에서 데이터를 로드해 Util을 Builder 해주는 WordFactoryBuilder - 금지어, 예외단어를 처리하는 BanWordUtil, ExceptWordUtil - 문자열 처리를 담당하는 WordUtil - 사용자가 사용하는 Inspector 이렇게 나누어져있는걸 볼 수 있습니다. 이런 역할을 분리하고, 인터페이스화, 추상화 단계를 직접 처리하며 기능분리에대한 이해도가 높아진것같습니다.
외부로 부터 주입받는 데이터 가장 고심했던게 InspectConfig 를 사용하는 것이었습니다. 사용자가 인터페이스를 상속하지 않더라도 문제가 없어야하고, 상속했다면 데이터를 넣어주어야 하는것에 대해서 어떻게 해야하는지 고민이 많았습니다. 그 결과로 required = false의 setter 주입을 통해 이 문제를 해결했고, null 체크를 해서 데이터를 외부로부터 주입받는 방법으로 이 문제를 해결했습니다.
뭐 아무튼 그렇고, 이 문자열 검사기는 아직 미완성입니다. 완성이 되면 Maven Central Repository 에 업로드할 예정입니다.
9월 30일 업데이트! WordLoader 객체를 삭제해서 json 파일을 더 이상 사용하지 않게 변경되었고, https://mvnrepository.com/artifact/io.github.kimseungwo/wordinspector 0.0.10 버전부터 적용되었습니다.
public interface WordUtil {
default void addWord(String word) { push(word, 0);}
void push(String word, int index);
int find(String str, int index, int deep, boolean ignoreSpace);
}
@Component
public class WordUtilImpl implements WordUtil {
private final Map<Character, WordUtilImpl> data = new HashMap<>();
@Override
public void push(String word, int index) {
if (word.length() <= index) return;
char c = word.charAt(index);
if (!data.containsKey(c)) data.put(c, new WordUtilImpl());
data.get(c).push(word, index + 1);
}
@Override
public int find(String str, int index, int deep, boolean ignoreSpace) {
WordUtilImpl wordUtil = this;
while (true) {
if (wordUtil.data.isEmpty()) return deep == 0 ? -1 : deep;
if (str.length() <= index) return -1;
if (ignoreSpace && str.charAt(index) == ' ') {
if (deep == 0) return -1;
index++; deep++;
continue;
}
if (!wordUtil.data.containsKey(str.charAt(index))) return -1;
wordUtil = wordUtil.data.get(str.charAt(index));
index++; deep++;
}
}
}
함수형 인터페이스는 1개의 추상메소드를 가지고 있는데요. 당연하게도 아무런 역할을 하지 않는 인터페이스입니다.
java.util.function
이런 함수형 인터페이스는 function 패키지에 담겨져있습니다.
그 내용을 열어보면
많은 인터페이스들이 존재합니다. 이 함수형 인터페이스에는 똑같은 특징이 있는데 @FunctionalInterface 어노테이션을 사용하고있다는 겁니다. 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해준다고 하는데요. 그렇지만 이 어노테이션을 사용하지 않더라도 동작하는데에는 문제가 없으니 참고해주시면 될것 같습니다.
함수형 인터페이스 정의
추상메소드가 단 한개인 인터페이스
static, default 메소드가 여러개여도 추상메소드가 단 한개인 인터페이스
람다표현식
자바 8 이상을 사용하시는 분들은 lambda 에 대해서 익숙하실 겁니다. 함수형 인터페이스의 목적은 람다표현식을 이용해 함수형 프로그래밍을 구현하기 위해서라고 할 수 있습니다.
함수형 인터페이스 종류
function 패키지 이외에 Callable, Comparator 등의 함수형 인터페이스가 있지만 이 글에서는 function 패키지 내에서만 설명하겠습니다.
인터페이스
메소드
설명
Runnable
void run()
매개변수를 받지 않고, 반환하지 않습니다. Thread 에서 Runnable 을 사용하고있습니다.
Consumer<T>
void accept(T t)
매개변수를 받고, 리턴하지 않을때 사용합니다. forEach 메소드에서 이 인터페이스를 사용하고있습니다.
Function<T, R>
R apply(T t)
제네릭 첫번째 타입을 매개변수로 받고,제네릭 두번째 타입을 반환합니다. Stream 에 map 메소드에서 사용하고있습니다.
Predicate<T>
boolean test(T t)
매개변수를 받아 true / false 인지를 반환합니다. Stream 에 filter에서 사용하고있습니다.
Supplier<T>
T get()
매개변수를 받지않고, 어떤 값을 반환합니다. Optional에 or(),orElseThrow() 등 메소드에서 사용하고있습니다.
1. Runnable
함수형 인터페이스 중에서 가장 애매한게 Runnable 이라고 생각합니다. 매개변수를 받지도 않고 반환값도 없는 이 인터페이스는 도대체 어디에 사용되는걸까요?
Thread 는 Runnable을 생성자 파라미터로 받아 사용하고 있는 것을 확인할 수 있습니다.
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
});
t.start();
Thread 에서 어떤 메소드를 실행할지에 대한 표현을 Runnable 를 사용하여 구현하였습니다. 이 Runnable 내에는 어떠한 함수가 와도 타입으로부터 자유롭습니다.
이 Consumer 는 forEach 메소드에서 사용하는 것 외에는 어디다가 이 메소드를 사용해야 유용하지? 라는 의문이 들었습니다. 고민을 하다가 Consumer 인터페이스의 변수명을 이용해보자라는 생각이 들었는데요. 하나의 작업을 Consumer로 묶으면 변수명을 보고 로직의 이유를 바로 유츄가 가능하다는 점을 이용해보았습니다.
물론 위에처럼 무턱대고 저렇게 하지는 않겠지만, 적절한 예시가 생각이 나지 않아 일단 이렇게 예시를 만들어봤습니다.
예를들어, 취소요청이 들어온 주문 건에 대한 비즈니스로직을 작성하려고합니다.
1. 사용된 쿠폰은 다시 사용할 수 있게 변경되어야 하고, 현재시간 기준 만료된 쿠폰은 삭제되어야한다.
2. 남은 금액은 환불되어야한다.
3. 환불건에 대해서 기록이 남아야한다.
4. 주문건은 삭제되지않고 기록을 남겨둔다.
라는 요청이라고 가정했을 때, 위와같이 Consumer 를 이용해서 각 기능별로 나눈다면 해당 작업에 대한 로직을 변수명을 보고 유추하기 쉽고, 작업의 순서를 파악하기도 쉬워졌습니다. 이렇게 로직을 분리해 결합도를 낮춰 사이드 이펙트에 대한 문제도 조금 해소되는것 같습니다.
추가로 andThen의 반환타입은 Consumer<T> 이기 때문에 이어 붙히는것이 가능합니다. 따라서 위의 코드를 더 정리한다면
public static void main(String[] args) {
List<Order> orders = service.getOrders();
Consumer<List<Order>> consumer = service.getConsumeCancelOrder();
consumer.accept(orders);
}
public class Service {
private static final Repository repository = new Repository();
public Consumer<List<Order>> getConsumeCancelOrder() {
Consumer<List<Order>> 사용한쿠폰중만료된쿠폰삭제 = repository.consumeDeleteCoupon();
Consumer<List<Order>> 금액환불 = repository.consumeRefund();
Consumer<List<Order>> 영수증생성 = repository.consumeSaveReceipt();
Consumer<List<Order>> 주문상태변경 = repository.consumeChangeCancelStatus();
return 사용한쿠폰중만료된쿠폰삭제.andThen(
금액환불.andThen(
영수증생성.andThen(
주문상태변경)));
}
}
accept와 Consumer 를 분리하는 것도 괜찮은것같습니다. 물론 getConsumeCancelOrder 를 호출할때마다 새로 생성할 필요가 없으므로 Spring의 Bean이나 static, Singleton 으로 관리하는 것이 옳지만 여기까지만 설명하도록 하겠습니다.
MVC 패턴은 개발자라면 한번은 들어봤을겁니다. 오늘은 MVC 패턴에 대해서 알아보겠습니다.
MVC 패턴이란?
모델-뷰-컨트롤러(Model-View-Controller, MVC)는 소프트웨어 디자인 패턴 중 하나입니다. 이 패턴은 사용자 인터페이스(UI)로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있는 애플리케이션을 만들 수 있습니다. 쉽게 말해 비즈니스 로직과 화면을 구분하는데 중점을 둔다는건겁니다.
MVC는 여러 파생되는 패턴을 가지고 있는데요. MVVM(Model-View-ViewModel), MVP(Model-View-Presenter), MVW(Model-View-Whatever)가 있습니다. 이 글에서는 MVC 에 대해서 알아보도록 하겠습니다.
Model, View, Controller의 관계
Model : 모델은 애플리케이션의 핵심 데이터와 비즈니스 로직을 나타냅니다. DB와의 상호작용, 데이터 처리 및 유효성 검사와 같은 작업을 수행합니다. 모델은 독립적으로 작동하며, 뷰와 컨트롤러와 직접적으로 통신하지 않습니다.
View : 사용자 인터페이스(UI)를 담당합니다. 모델에서 데이터를 받아 사용자에게 표시하고, 입력을 컨트롤러에 전달하는 역할을 합니다.
Controller : 사용자의 입력을 처리하고, 애플리케이션의 흐름을 관리합니다. 모델을 호출하여 데이터를 조작하거나 변경된 결과를 뷰에 전달하는 역할을 합니다.
출처 : wikipeida - 모델-뷰-컨트롤러
MVC 패턴을 설명할때 많이 사용하는 흐름도를 가져왔습니다.
이 개념을 웹에 적용한다면,
USES : 사용자의 입력을 감지하고 Controller로 전달한다.
MANIPULATES : Controller는 사용자가 요청한 웹 페이지를 보여주기 위해 Model 호출한다.
UPDATES : 비즈니스 로직을 통해 데이터를 제어한 후 결과를 Controller에 반환하고 다시 Controller는 Model의 결과를 받아 View에 전달합니다.
SEES : 데이터가 반영된 View가 사용자에게 보여준다.
MVC 패턴의 설계원칙
1. 각 구성 요소의 역할과 책임을 명확하게 구분
Model, View, Controller는 독립적으로 작동하고, 각각의 역할에 집중해야 합니다.
2. 구성 요소간의 결합도 최소화
구성 요소간의 직접적인 참조를 피해 의존성을 낮춰야합니다. 이를 통해 구성 요소를 독립적으로 개발, 수정, 테스트할 수 있습니다.
3. 코드의 재사용성과 확장성 고려
각 구성 요소는 독립적이고 재사용 가능한 모듈로 개발되어야합니다. 프로젝트의 규모가 코지거나 요구 사항이 변경되었을 때, 확장 및 수정이 용이해야합니다.