ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 1. 관심사의 분리(SOC: Seperation Of Concern) 원칙
    소프트웨어 설계 원칙/2. 기본 원칙 2026. 5. 19. 15:44

    관심사의 분리 원칙이란?

    간단하게 관심이 비슷한 것들끼리는 모으고 관심이 다른 것끼리는 분리한다는 원칙이다.

     

     

    들어가기 전에

    물론 여태 공부를 하면서 SOLID가 무엇인지, 추상화가 무엇인지, DRY가 무엇인지, MVVM 아키텍처가 무엇인지, DDD가 무엇인지 등 무척이나 많은 글을 접했었다.

    수 많은 글을 읽고 표면적으로는 이해했었지만 

    "왜 자꾸 비슷한 원칙들이 반복돼서 언급이 되지?", "그래서 무엇을 쓸지 어떻게 판단해야하지?", "도대체 어떤 것을 우선시 해야하는 거지?" 등등 의문만 깊어지고 본질을 알지 못한채 계속 생겨나는 지식들을 머릿속에 끝없이 주입만 하는 것 같은 기분이 들었다.

    하지만 이번 기회에

    그 동안 그저 주입식으로 듣고 공부해 왔던 디자인 패턴, 아키텍처, 여러 휴리스틱과 SOLID 등이 무엇이고 왜 생겨났는지, 조금 더 이해하고 정리할 수 있게 되었다.

     

     

    조금 더 구체적으로 들어가보자

     

    - 관심사 분리 전 코드

    // app/signup/page.tsx
    'use client';
    
    import { useState } from 'react';
    import { useRouter } from 'next/navigation';
    
    export default function SignupPage() {
        const router = useRouter();
        const [email, setEmail] = useState('');
        const [password, setPassword] = useState('');
        const [error, setError] = useState('');
    
        const handleSubmit = async () => {
            // 검증
            if (!email.includes('@')) {
                setError('올바른 이메일 형식이 아닙니다');
                return;
            }
            if (password.length < 8) {
                setError('비밀번호는 8자 이상이어야 합니다');
                return;
            }
    
            // API 호출
            try {
                const res = await fetch('/api/signup', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ email, password }),
                });
                const data = await res.json();
    
                if (!res.ok) {
                    setError(data.error_message);
                    return;
                }
    
                // 분석 이벤트
                fetch('/api/analytics', {
                    method: 'POST',
                    body: JSON.stringify({
                        event: 'signup_success',
                        user_id: data.user_info.user_id,
                        timestamp: new Date().toISOString(),
                    }),
                });
    
                // 페이지 이동
                router.push('/welcome');
            } catch (e) {
                setError('네트워크 오류가 발생했습니다');
            }
        };
    
        return (
            <div>
                <h1>회원가입</h1>
                <input
                    type="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    placeholder="이메일"
                />
                <input
                    type="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    placeholder="비밀번호"
                />
                {error && <p>{error}</p>}
                <button onClick={handleSubmit}>가입하기</button>
            </div>
        );
    }

     

    한 컴포넌트 안에 5가지 다른 종류의 일이 섞여 있다:

    • 검증 로직 (이메일 형식, 비밀번호 길이)
    • API 호출 (signup)
    • 분석 이벤트 전송
    • 페이지 이동
    • UI 렌더링과 상태 관리

     

    - 관심사 분리 후 코드

    // app/signup/page.tsx
    'use client';
    
    import { useState } from 'react';
    import { useRouter } from 'next/navigation';
    import { validateSignup } from '@/utils/signupValidation';
    import { signup } from '@/api/auth';
    import { trackEvent } from '@/services/analytics';
    
    export default function SignupPage() {
        const router = useRouter();
        const [email, setEmail] = useState('');
        const [password, setPassword] = useState('');
        const [error, setError] = useState('');
    
        const handleSubmit = async () => {
            const validation = validateSignup(email, password);   <<< ✅분리
            if (!validation.ok) {
                setError(validation.reason);
                return;
            }
    
            try {
                const user = await signup(email, password);   <<< ✅분리
                trackEvent({ type: 'signup_success', metadata: { userId: user.id } }); <<< ✅분리
                router.push('/welcome');
            } catch (e) {
                setError(e instanceof Error ? e.message : '오류가 발생했습니다');
            }
        };
    
        return (
            <div>
                <h1>회원가입</h1>
                <input
                    type="email"
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                    placeholder="이메일"
                />
                <input
                    type="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    placeholder="비밀번호"
                />
                {error && <p>{error}</p>}
                <button onClick={handleSubmit}>가입하기</button>
            </div>
        );
    }
    • 검증 로직 (이메일 형식, 비밀번호 길이)
    • API 호출 (signup)
    • 분석 이벤트 전송

    이 세 관심을 분리하였다. 때에 따라서는 

    • 페이지 이동
    • UI 렌더링과 상태 관리

    도 분리할 수 있다.

     

    너무 모호한데?

    이처럼 관심사에 따라 분리한다는 기본 원칙이지만

    나는 이 원칙을 보고 살짝 혼란스러웠다.

    왜냐하면 당장 위의 예시만하더라도 UI Form을 따로 분리할 수도 있고, 하지 않을 수도 있다.

    분리를 한다면 여러 useState도 같이 Form 내부로 옮겨야 할지, props로 내려야할지 고민이 된다.

    물론 리액트의 관점에서 보면 렌더링을 최소화하기 위해 Form 내부에 작성하는 것이 옳아 보인다.

    하지만 추후에 확장성을 고려한다면? 그리고 그 외 여러가지를 고려해본다면?

    코드를 짜는 방법은 무궁무진하다.

    이 무궁무진한 방법 안에서 무엇을 우선으로 생각해보아야 하는 것인지 머릿속이 복잡해진다.

     

    하지만 찾아본 결과

    이 행동은 모두 '유지 보수의 용이'를 위함이라는 것을 알게 되었다.

    하지만 유지보수가 용이한 코드는 너무 각양각색으로 달라질 수 있다.

    이 무궁무진한 방법 중에서 어떤 방향으로 짜야 할지 기준이 필요하다.

    '어떻게 구현할 것인가'

    유지보수를 위한 코드를 작성하기 위한 행동 지침이 있다.

    SoC라는 큰 방향성 아래에서, 이를 실현하기 위한 원칙들이다.

    그것이 다음에 설명할

    • 추상화
    • 정보은닉
    • 캡슐화

    이다. 

    이런 원칙을 지키며 코드를 구현해도 너무나 많은 방식이 있을 수 있다.

    그래서 구현 결과가 잘 됐는지 측정하는 척도가 필요한데, 그것이

    • 응집도와 결합도

    이다. 같은 원칙을 더 단순한 형태로 표현한 휴리스틱들도 있다.

    • DRY (반복하지 마라)
    • KISS (단순하게 유지하라)
    • YAGNI (필요할 때까지 만들지 마라)

    등이 그 예시이며

    특히 객체지향 프로그래밍(OOP)에서 자주 쓰이는 5가지 원칙인 SOLID는 위의 원칙들을 OOP 관점에서 구체화한 표준이다

     

    정리해서

    SoC라는 큰 방향성 아래에서

    • 행동 지침: 추상화 / 정보 은닉 / 캡슐화
    • 측정 도구: 결합도 / 응집도
    • 단순화된 휴리스틱: DRY / KISS / YAGNI
    • OOP 표준: SOLID

    라고 이해할 수 있다.

     

     

    디자인 패턴과 아키텍처

    디자인 패턴과 아키텍처도 같은 맥락이다.

    모두 SoC라는 원칙을 어떻게 구체적으로 실현할지에 대한 답들이다.

     

    • 디자인 패턴 : 코드를 어떻게 구현할 것인가, 여러 상황에서 사용할 수 있는 여러가지 검증된 패턴들
    • 아키텍처 : 소프트웨어나 프로젝트의 뼈대, 구조, 설계

     

    라고 볼 수 있다.

    SoC는 '관심사를 분리하라'는 원칙이지만, 어느 정도로 분리할지, 어떻게 분리할지에 대한 절대적 기준이 없다.

    프로젝트 규모, 변경 빈도, 팀 구조에 따라 적절한 분리 정도가 달라진다.

    프로젝트나 소프트웨어 관점에서 전체적으로 어떻게 분리하여 설계할지에 따라 아키텍처의 종류가 결정되고,

    소프트웨어나 프로젝트 내에서 여러 기능들을 구현할 시, 어떻게 나눠서 구현할 지에 따라 디자인 패턴이 결정이 된다.

     

    그리고 이는 모두 기본 원칙 위에서 코드의 유지보수를 용이하게 하기 위함이다.

     

    '소프트웨어 설계 원칙 > 2. 기본 원칙' 카테고리의 다른 글

    4. 캡슐화(Encapsulation)  (0) 2026.05.22
    3. 정보 은닉(Information Hiding)  (0) 2026.05.22
    2. 추상화(Abstraction)  (0) 2026.05.20
Designed by Tistory.