728x90

아직 배운 것은 많이 없지만 그래도 예제(학생 관리 예제)를 하나 작성 해보자

 

소프트웨어설계에서 공부한 대로

 

비즈니스 요구사항 정리

학생 도메인과 레포지토리 만들기

학생 레포지토리 테스트 케이스 작성

학생 서비스 개발

 

의 순서로 진행이 될 예정이다.

 

  • 비즈니스 요구사항 정리

우선 비즈니스 요구사항을 정리하자면

 

학교라고 설정을 하고

 

데이터는 학번, 이름

기능은 학생 등록, 조회

이 정도만 구현을 해보도록 하자

 

일반적으로 구조는

컨트롤러는 저번에 만들어 보았던 대로 웹 MVC에서 C의 컨트롤러이다.

서비스는 비즈니스에 관련된 로직을 구현한다.

리포지토리는 데이터베이스에 접근하며, 객체를 DB에 저장하고 관리한다.

 

아직 리포지토리에서 어떤 저장소를 사용할지 결정하지 않아서 interface로 클래스를 변경할 수 있도록 설계한다.

  • 학생 도메인과 리포지토리 만들기

당연히 프로젝트를 생성하고

클래스를 작성할 패키지를 생성한 후 작성을 해준다.

package spring_practice.spring_practice.domain;

public class Member {
    
    private Long studentId; //학번
    private String name; //이름

    public Long getStudentId() {
        return studentId;
    }

    public void setStudentId(Long studentId) {
        this.studentId = studentId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

이렇게 학번과 이름을 가지고 있는 클래스를 만들고, command + n을 이용하여 getter와 setter를 작성해 준다.

 

이번엔 리포지토리를 만들자, 아직 선정되지 않았으니 인터페이스를 만들고 구현을 하도록 해야 한다.

package repository;

import domain.Member;

import java.util.List;
import java.util.Optional;

public interface MemberRepository {
    Member save(Member member); //학생을 추가하는 메서드
    Optional<Member> findById(Long id); //학번을 이용하여 학생을 찾는 메서드
    Optional<Member> findByName(String name); //이름을 이용하여 학생을 찾는 메서드
    List<Member> findAll(); //지금까지 저장된 학생을 LIST 로 받아오는 메서드
}

 

일단 이 인터페이스를 구현할 class를 작성한다.

package spring_practice.spring_practice.repository;

import spring_practice.spring_practice.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>(); //학생들의 정보를 저장할 Map
    private static Long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setStudentId(++sequence);
        store.put(member.getStudentId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
        store.clear();
    }
}

학생들의 데이터는 일단 Map을 이용하여 저장을 한다.

 

  • 학생 리포지토리 테스트 케이스 작성

일단 여기까지 하고 바로바로 테스트를 진행해 보자.

test 케이스는 test 폴더 밑에 작성을 한다.

 

그리고 보통 테스트 케이스의 패키지는 기존 패키지와 똑같이 작성을 한다.

그리고 테스트할 class의 파일명에 Test를 붙여서 테스트 케이스의 이름을 작성한다.

 

간단하게 command + shift + T로 만들 수도 있다.

여기 안에 테스트 코드들을 작성해야 한다.

각 메서드들을 보면 @Test annotation이 달려있는 것을 볼 수 있다.

 

class 내에서 윗부분에

    MemoryMemberRepository repository = new MemoryMemberRepository();
    
    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }

이 코드를 추가하자

repository는 테스트하면서 사용할 리퍼지토리이고 afterEach메서드는 각각의 메서드를 테스트할 때마다 리퍼지토리를 비워즈는 메서드이다.

@AfterEach annotation을 달아서 각 테스트 후에 실행이 되도록 해준다.

 

차례로 save 메서드부터 테스트해 보자

save 메서드는 새로운 학생을 추가하는 메서드이다.

    @Test
    void save() {
        //given
        Member member = new Member();
        member.setName("seungkyu");
        
        //when
        repository.save(member);
        
        //then
        Member result = repository.findById(member.getStudentId()).get();
        Assertions.assertEquals(member, result);
    }

테스트 케이스는 given, when, then 상황 3가지로 나누어 작성을 하고

seungkyu라는 이름을 가진 멤버를 추가하고 repository에서 학번으로 해당 멤버를 찾은 후 Assert로 둘이 일치하는지 확인하는 테스트이다.

 

이렇게 작성을 한 후 run을 하면

이렇게 문제없이 초록색으로 보이게 된다.

 

이번엔 이름을 이용해서 가져오는 findByName을 테스트하자

    @Test
    void findByName() {
        //given
        Member member1 = new Member();
        member1.setName("seungkyu1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("seungkyu2");
        repository.save(member2);

        //when
        Member result = repository.findByName("seungkyu1").get();

        //then
        Assertions.assertEquals(member1, result);
        Assertions.assertNotEquals(member2, result);
    }

이름을 이용해 가져와보고 member1과 result가 같은지, member2와 result가 같지 않은지를 확인해 본다.

테스트를 수행하면 정상적으로 초록불이 나올 것이다.

 

마지막으로 전체를 가져오는 findAll이다.

@Test
    void findAll() {
        //given
        Member member1 = new Member();
        member1.setName("seungkyu1");
        repository.save(member1);
        
        Member member2 = new Member();
        member2.setName("seungkyu2");
        repository.save(member2);
        
        //when
        List<Member> result = repository.findAll();
        
        //then
        Assertions.assertEquals(2, result.size());
    }

이렇게 작성을 하면 2개의 객체를 추가했으니 사이즈는 2가 되어야 한다.

AfterEach를 수행하고 테스트를 수행하기 때문에 정상적으로 초록불이 들어와야 한다.

 

  • 학생 서비스 개발

비즈니스에 가까운 서비스를 개발해 보자.

Service 패키지를 따로 만든 후

 

우선 memberRepository를 생성자를 이용해 주입할 수 있도록 만들고

package spring_practice.spring_practice.service;

import spring_practice.spring_practice.repository.MemberRepository;

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }   
}

이렇게 DI가 가능하도록 설계를 해야 나중에 리퍼지토리를 변경할 때 적은 코드를 수정할 수 있게 된다.

 

package spring_practice.spring_practice.service;

import spring_practice.spring_practice.domain.Member;
import spring_practice.spring_practice.repository.MemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

    private void validateDuplicateMember(Member member){ // 같은 이름의 학생이 있는지 찾아주는 메서드
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 등록된 학생입니다.");
                });
    }

    public Long join(Member member){
        validateDuplicateMember(member);
        memberRepository.save(member); // 이미 등록된 학생이 아니면 저장
        return member.getStudentId();
    }

    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findStudent(Long studentId){
        return memberRepository.findById(studentId);
    }
}

원래는 같은 이름을 가진 학생들이 있겠지만 학습을 위해 같은 이름의 학생을 등록할 수 없도록 만들어보자.

 

이해하기 어렵지 않을 것이고 바로 테스트 케이스를 만들어보자.

당연히 command + shift + T로 만들것이며 AfterEach에 이어서 테스트가 수행되기 전에 실행되는 BeforeEach를 사용할 것이다.

 

이 서비스는 생성자를 통해 외부에서 리퍼지토리를 주입해주어야 했다.

그렇기에 BeforeEach를 통해 각 테스트 실행 전마다 주입해 줄 예정이다.

package spring_practice.spring_practice.service;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import spring_practice.spring_practice.repository.MemoryMemberRepository;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    
    @Test
    void join() {
    }

    @Test
    void findMembers() {
    }

    @Test
    void findStudent() {
    }
}

 

나머지 코드들을 작성해보자.

package spring_practice.spring_practice.service;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import spring_practice.spring_practice.domain.Member;
import spring_practice.spring_practice.repository.MemoryMemberRepository;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }

    @Test
    void join() throws Exception{
        //given
        Member member = new Member();
        member.setName("seungkyu");

        //when
        Long stuId = memberService.join(member);

        //then
        Member findMember = memberRepository.findById(stuId).get();
        Assertions.assertEquals(member, findMember);
    }

    void dupliExc() throws Exception{
        //given
        Member member1 = new Member();
        member1.setName("seungkyu");

        Member member2 = new Member();
        member2.setName("seungkyu");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        Assertions.assertEquals(e.getMessage(), "이미 등록된 학생입니다.");
    }
}

저렇게 seungkyu라는 이름의 학생이 두명이니 중복 예외가 발생해야 할 것이고 그것까지 테스트하는 코드이다.

'Spring > 스프링' 카테고리의 다른 글

스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04
스프링 4일차  (0) 2023.02.03
스프링 3일차  (0) 2023.02.01
728x90

오늘은 클라이언트와 서버의 연결을 유지시켜주는 방법인 Cookie에 대해서 알아본다.

 

  • Cookie란?

서버와 클라이언트의 연결에 대한 흔적을 남기는 것을 Cookie라고 한다.

다음에 연결할 일이 생긴다면 이 Cookie를 참고하게 된다.

  • Cookie 구현

Cookie[] cookies = request.getCookies();
Cookie cookie = null;

for(Cookie c : cookies){
	System.out.println("c.getName(): " + c.getName() + ", c.getValue() :" + c.getValue());
    
    if(c.getName().equals("memberId")){
    	cookie = c;
	}
}

if(cookie == null){
	System.out.println("cookie is null");
    cookie = new Cookie("memberId", mId);
}

response.addCookie(cookie);
cookie.setMaxAge(60 * 60);

response.sendRedirect("loginOk.jsp");

 

이렇게 Cookie를 찾아보고 없으면 새로운 Cookie를 만드는 방식으로 구현을 한다.

 

이제 실습을 해보도록 하자, 바로 프로젝트를 생성을 해주면 된다.

우선 jsp로 페이지를 먼저 만들도록 하자.

loginCon으로 처리를 하도록 페이지를 작성했다.

 

그럼 해당 데이터를 받는 loginCon Servlet을 작성해보자

해당 데이터를 받고 클라이언트에서 모든 Cookie를 검사하고 만약 memberId에 해당하는 쿠키가 없다면 쿠키를 생성해준다.

쿠키의 유지 시간을 60 * 60초로 갱신을 해주고

loginOk.jsp로 보낸다.

loginOk.jsp이다

Cookie 배열에 있는 모든 쿠키를 출력해준다.

 

이제 다시 login.jsp로 돌아가야 한다.

 

만약 쿠키가 남아있다면 login을 하지 않아도 접속이 될 수 있도록 만들어야 할 것이다.

그렇기 때문에

윗 부분에 서블렛 태그로 만약 memberId에 해당하는 Cookie가 있다면 loginOk.jsp를 실행해주는 코드를 추가해준다.

 

그렇다면 만약 쿠키가 남아있다면

이런 화면이 출력되게 된다.

 

이렇게 웹에서 쿠키를 남기는 방법에 대해 알아보았다.

'Spring > JSP, Servlet' 카테고리의 다른 글

JSP & Servlet 12일차  (0) 2023.02.05
JSP & Servlet 11일차  (0) 2023.02.05
JSP & Servlet 9일차  (0) 2023.01.15
JSP & Servlet 8일차  (0) 2023.01.04
JSP & Servlet 7일차  (0) 2023.01.04
728x90

시스템 프로그래밍 3주차는 2주차의 내용에 연장으로 표준 입출력에 대해 공부한다.

 

  • Standard IO

Standard IO는 platform에 독립적인, user-buffering solution이다.

 

File pointer

File operation을 관리하는 구조체를 가리키는 포인터

내부적으로 file descriptor와 연동됨

 

Stream

프로그램과 file을 연결한 통로

 

파일 IO에 대한 일의 순서는

File open -> File access -> File close로

파일을 열고 사용을 한 후 파일을 닫는 순서이다.

 

  • File open & close

Opening a file/stream

#include <stdio.h>

FILE *fopen(const char *path, const char *mode);

-path(file path)

열려는 파일의 경로

-mode(file open mode)

파일 열기 모드

return: file pointer (NULL: error)

 

파일 열기 모드(mode)

  모드 파일이 존재하지 않을 때 파일이 존재 할 때
r 읽기 열기 실패 열기 성공
w 쓰기 파일이 생성 덮어 씀(기존 내용은 지워 짐)
a 덧붙이기 파일이 생성 기존 파일 뒤에 기록
r+ 읽기+ 모드 / 쓰기 모드로 전환 가능 읽기 <-> 쓰기 모드 전환 시,
반드시 fflush(), fseek(), fstepos(), rewind() 중 하나를 호출해야 함
w+ 쓰기+ 모드/  읽기 모드로 전환 가능
a+ 덧붙이기+ / 읽기 모드로 전환 가능
b 이진 파일 모드 이진 파일 형식으로 파일을 연다
읽기/쓰기 모드는 위의 지정자들로 지정

이진 파일은 사람이 읽을 수 있는 문자로 채워진 Ascii 파일과 다르게 이진 데이터가 직접 저장된 파일을 말한다.

 

Closing a files/streams

#include <stdio.h>

int fclose(FILE *stream);

-stream

닫으려는 stream

return: 0(-1: error)

 

열고 닫는 방법을 배웠으니 바로 한 번 해보도록 하자.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    FILE *fp;

    if ((fp = fopen("hello.txt", "w")) == NULL){
        perror("fopen: hello.txt");
        exit(1);
    }

    fclose(fp);

    return 0;
}

딱 파일만 열고 바로 닫는 예제이다.

실행을 해보면 w 모드이기 때문에 

없던 hello.txt 파일이 생성되는 것을 볼 수 있다.

 

  • File read & write

파일을 열고 닫는 방법을 공부했으니 이제 순서의 중간인 파일의 사용에 대해 공부해보자

 

Character-based

Chararcter-based reading

#include <stdio.h>

int fgetc(FILE *stream);
int getc(FILE *stream); //macro 형태로 구현되어 있어, 인자에서 계산은 X
int getchar(void); //getc(stdin), 표준입력에서 입력을 받음

-stream

File operation을 수행할 stream

return : 읽은 문자 (-1: error)

 

파일로부터 문자 하나씩 읽는 방법이다.

 

Character-based writing

#include <stdio.h>

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c); //putc(c, stdout)

-stream

File operation을 수행할 stream

-c

쓰려는 문자

return: 기록한 문자(-1:error)

 

Characted-based로 실습을 해보자

#include <stdlib.h>
#include <stdio.h>

int main(void){
    FILE *rfp, *wfp;
    int c;

    if((rfp = fopen("hello.txt", "r")) == NULL){
        perror("fopen: hello.txt");
        exit(1);
    }

    if((wfp = fopen("hello.out", "w")) == NULL){
        perror("fopen: hello.out");
        exit(1);
    }

    while((c = fgetc(rfp)) != EOF){ //하나씩 계속 읽어서 hello.out에 작성
        fputc(c, wfp);
    }

    fclose(rfp);
    fclose(wfp);

    return 0;
}

이렇게 파일을 작성하면 hello.txt에서 문자를 하나씩 읽어서 hello.out에 저장하게 된다.

String-based

String-based reading

#include <stdio.h>

char *gets(char *s); //get from stdin
char fgets(char *s, int n, FILE *stream);

-s

읽은 문자열을 저장할 buffer

-n

buffer의 크기

-stream

File operation을 수행할 stream

return: Buffer의 시작 주소(NULL: error)

 

String-based writing

#include <stdio.h>

int puts(char *s); //put to stdout
int fputs(char *s, FILE *stream);

-s

기록할 문자열을 저장한 buffer

-stream

File operation을 수행할 stream

return: 양수(음수: error)

 

문자열 단위로 실습을 해보자

#include <stdio.h>
#include <stdlib.h>

int main(void){
    FILE *rfp, *wfp;
    char buf[BUFSIZ];
    printf("BUFSIZ = %d\n", BUFSIZ);

    if((rfp = fopen("hello.txt", "r")) == NULL){
        perror("fopen: hello.txt");
        exit(1);
    }

    if((wfp = fopen("hello.out", "a")) == NULL){
        perror("fopen: hello.txt");
        exit(1);
    }

    while(fgets(buf, BUFSIZ, rfp) != NULL) fputs(buf, wfp);

    fclose(rfp);
    fclose(wfp);

    return 0;
}

문자열 단위로 읽어와서 hello.out에 저장한다.

이렇게 문자열 단위로 잘 가져오는 것을 볼 수 있다.

 

Binary IO

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

-ptr

Pointer to buffer

-size

size of an item

-nmemb

number of items to read/write

-stream

File operation을 수행 할 stream

return: read/write 한 item의 수(EOF: 파일의 끝)

 

이진 파일을 작성하는 예제이다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    char *fileName = "binary.bin";
    int data[5] = {10, 20, 30, 40, 50};
    FILE *fp;

    if(!(fp = fopen(fileName, "wb"))){
        fprintf(stderr, "Fail to open the file - %s\n", fileName);
        exit(1);
    }

    size_t i = fwrite(data, sizeof(int), 5, fp);
    printf("Success to write %d objects.\n", i);

    fclose(fp);
    return 0;
}

그러고 cat으로 파일을 확인해보면 이진파일이기 때문에 알 수 없는 내용이 출력된다.

해당 이진파일의 내용을 그대로 보고 싶다면

xxd binary.bin을 사용하면 된다.

이제 이진파일을 읽어보자

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int buf[5] = {0};
    FILE *fp = fopen("binary.bin", "rb");

    if(!fp){
        fprintf(stderr, "Fail to open the file - %s\n", "binary bin");
        exit(1);
    }

    size_t i = fread(buf, sizeof(int), 5, fp);
    printf("Success to read %d object\n", i);
    for(int i = 0; i < 5; i++) printf("%d ", buf[i]);

    fclose(fp);
    return 0;
}

이렇게 이진파일을 정수로 읽어오는 것을 볼 수 있다.

이진 파일은 사람이 바로 읽을 수 없지만 동일한 데이터를 저장하는 데 Ascii 파일보다 적은 공간을 요구한다는 장점이 있다.

 

Formatted IO

#include <stdio.h>

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);

-format

입출력 형식

-stream

File operation을 수행 할 stream

return: 입출력 한 문자의 수(음수: error)

 

자세히 볼 필요도 없을만큼 많이 사용한 함수들이다.

바로 작성을 하는 예제이다.

#include <stdio.h>
#include <stdlib.h>

typedef struct{
    int ID;
    char name[8];
    float score;
}Student;

int fileOpen(FILE **fp, char *fileName, char *mode){
    *fp = fopen(fileName, mode);
    if(!*fp){
        printf("Fail to open - %s\n", fileName);
        return -1;
    }
    return 0;
}

int main(void){
    Student info = {0};
    char *fileName = "StudentList.txt";
    FILE *fp = NULL;

    if(fileOpen(&fp, fileName, "a") < 0) exit(1);

    while(1){
        printf("Enter Id Name Score (Exit: -1): ");
        scanf("%d", &info.ID);
        if(info.ID < 0) break;
        scanf("%s %f", &info.name, &info.score); getchar();
        fprintf(fp, "%d %s %.1f\n", info.ID, info.name, info.score);
    }

    fclose(fp);

    return 0;
}

-1을 입력할 때까지 입력을 받고 StudentList.txt에 작성을 하는 예제이다.

이렇게 StudentList.txt를 만들어 그곳에 작성을 하는 것을 볼 수 있다.

 

이제 이 파일을 읽어보자

#include <stdio.h>
#include <stdlib.h>

typedef struct{
    int ID;
    char name[8];
    float score;
}Student;

int fileOpen(FILE **fp, char *fileName, char *mode){
    *fp = fopen(fileName, mode);
    if(!*fp){
        printf("Fail to open - %s\n", fileName);
        return -1;
    }
    return 0;
}

int main(void){
    Student info = {0};
    char *fileName = "StudentList.txt";
    FILE *fp = NULL;
    
    if(fileOpen(&fp, fileName, "r") < 0) exit(1);

    int numStudent = 0;
    float sum = 0;
    while(!feof(fp)){
        fscanf(fp, "%d %s %f\n", &info.ID, &info.name, &info.score);
        sum += info.score;
        numStudent++;
    }

    printf("%d students, Average = %2.f\n", numStudent, sum / numStudent);

    fclose(fp);
}

해당 파일을 잘 읽는 것을 볼 수 있다.

 

Synchronizing with the disk

#include <stdio.h>

int fflush(FILE *stream);

-stream

File operation을 수행 할 stream

return: 0(-1:error)

 

stream에 있는 내용들을 바로 디스크에 작성하는 방법

 

  • File offset & File pointer

Handling file offset

#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);
int ftell(FILE *stream);
void rewind(FILE *stream);
int fsetpos(FILE *stream, const fpost_t *pos);
int fgetpos(FILE *stream, fpost_t *pos);

-stream

File operation을 수행 할 stream

-offset

이동시킬 byte 수(양수, 음수 모두 가능)

-whence

기준 위치(SEEK_SET, SEEK_CUR, SEEK_END)

-pos

offset을 저장 할 fpost_t 주소

 

File Pointer <-> File Descriptor

#include <stdio.h>

FILE *fdopen(int fd, const char *mode);

-fd

file descriptor

-mode

파일 열기 모드

return: File pointer(NULL: error)

 

#include <stdio.h>

int fileno(FILE *stream);

-stream

변환 할 stream

return: File descriptor(-1: error)

 

서로 pointer와 descriptor간의 변환하는 함수이다.

728x90

Servlet에서 데이터를 공유하는 방법에 대해 공부하자

 

  • Servlet parameter

이렇게 늘 그렇게 했듯이 

xml에 작성된 데이터를 가져오는 방법이다.

 

당연히 한 번 해보도록 하자

프로젝트를 생성한 후

web.xml에 servlet을 mapping 해주고

이렇게 init-parameter들도 넣어준다.

 

이러면 이제 Servlet으로 가서

이렇게 초기값들도 getServletConfig를 이용해 가져와본다.

 

실행을 해보면 당연히

이렇게 초기값들이 잘 출력이 되게 된다.

 

  • context parameter

이것도 jsp 때와 비슷하다.

전체적으로 공유될 수 있는 데이터이며

 

바로 사용을 해보자면

이렇게 xml에 context-param을 적어주고

이렇게 getServletContext로 가져와보면

잘 가져와지는 것을 확인 할 수 있다.

 

  • context attribute

JSP 때와 마찬가지로 데이터를 set하고 get하는 방법을 알아보자

이렇게 setAttribute를 작성해두고

이것을 가져와보기 위해 다른 Servlet을 작성해본다.

다른 Servlet에서 getAttribute를 이용해 데이터를 가져와보고

실행을 해보면

처음에는 이렇게 데이터들을 가져올 수 없어 null이 뜨게 된다.

setAttribute가 작성된 페이지를 켜본 후

새로고침을 해보면 이렇게 원하는 결과가 출력되는 것을 볼 수 있다.

'Spring > JSP, Servlet' 카테고리의 다른 글

JSP & Servlet 12일차  (0) 2023.02.05
JSP & Servlet 11일차  (0) 2023.02.05
JSP & Servlet 10일차  (0) 2023.01.16
JSP & Servlet 8일차  (0) 2023.01.04
JSP & Servlet 7일차  (0) 2023.01.04
728x90

2주차에는 파일 입출력에 대해 공부한다.

 

  • File

Unix/Linux에서의 file 타입은 이렇게 있다.

 

Regular file(일반 파일)

- text or binary data file

 

Directory

- 윈도우의 폴더이며 Unix/Linux에서는 directory도 하나의 파일로 본다

 

Special file(특수 파일)

- 파일 형태로 표현된 커널 내 객체

- 데이터 전송, 장치 접근 시 사용하는 파일

 

Basic commands for file

 

ls

현재 directory 내의 파일 목록 출력

주요 Option

-l : 상세 파일 정보 출력

-a: 모든 파일(숨겨진 파일 포함 목록 출력)

 

touch

빈 파일을 생성하거나 해당 파일이 있다면 time stamp를 변경해준다.

 

rm

파일 삭제

주요 Option

-r: directory 삭제

 

cat

파일 내용 출력

 

copy

파일 복사

주요 Option

-r: directory 복사

 

mv

파일을 이동하거나 이름을 변경

 

File access permission

Owner, Group, others 사용자에 따라

읽기(r), 쓰기(w), 실행(x) 권한을 가짐

3비트 3비트 3비트
소유자 접근 권한 그룹 접근 권한 기타 사용자 접근 권한
r w x r w x r w x
Bit Octal value Text value Corresponding permission
8 400 r - - - - - - - -  Owner may read
7 200 - w - - - - - - -  Owner may write
6 100 - - x - - - - - - Owner may execute
5 040 - - - r - - - - - Group may read
4 020 - - - - w - - - - Group may write
3 010 - - - - - x - - - Group may execute
2 004 - - - - - - r - - Everyone else may read
1 002 - - - - - - - w - Everyone else may write
0 001 - - - - - - - - x Everyone else may execute

권한은 이렇게 주어지며 권한은 변경할 수도 있다.

 

chmod

파일 권한 변경

가운데 권한을 비트연산으로 넣어준다.

 


  • Open and Close a file

Low-level file IO vs High-level file IO

Low-Level File IO(System call) High-Level File IO(Buffered IO)
System call을 이용해서 파일 입출력 수행 C Standard library를 사용해서 파일 입출력 수행
File descriptor 사용 File Pointer 사용
Byte 단위로 디스크에 입출력 버퍼(block) 단위로 디스크에 입출력
특수 파일에 대한 입출력 가능  

 

Opening files

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags [, mode_t mode]);

- pathname (file path)

열려는 파일의 경로 (파일 이름 포함)

- flags(file state flags)

파일을 여는 방법 설정

-mode (file access permission)

파일을 새로 생성할 때만 유효

return: file descriptor

 

아래는 flags이며 여러 플래그들을 조합 가능하다 (OR bit operation인 |을 사용)

자주 사용하는 flag들만 소개하겠다.

종류 기능
O_RDONLY 파일을 읽기 전용으로 연다
O_WRONLY 파일을 쓰기 전용으로 연다
O_RDWR 파일을 읽기와 쓰기가 가능하게 연다
O_CREAT 파일이
없으면 파일을 생성한다.

아래는 mode이다, flag와 마찬가지로 조합이 가능하며 파일 권한 설정 값을 사용한다.

플래그 모드 설명
S_IRWXU 0700 소유자 읽기/쓰기/실행 권한
S_IRUSR 0400 소유자 읽기 권한
S_IWUSR 0200 소유자 쓰기 권한
S_IXUSR 0100 소유자 실행 권한
S_IRWXG 0070 그룹 읽기/쓰기/실행 권한
S_IRGRP 0040 그룹 읽기 권한
S_IWGRP 0020 그룹 쓰기 권한
S_IXGRP 0010 그룹 실행 권한
S_IRWXO 0007 기타 사용자 읽기/쓰기/실행 권한
S_IROTH 0004 기타 사용자 읽기 권한
S_IWOTH 0002 기타 사용자 쓰기 권한
S_IXOTH 0001 기타 사용자 실행 권한

 

 

file descriptor

여기서 file descriptor는 열려 있는 파일을 구분하는 정수 값이다.

파일을 열 때 순차적으로 할당이 되며

Default fds

0: stdin

1: stdout

2: stderr

이기 때문에 보통 3번부터 할당이 된다.

 

File table

열린 파일을 관리 하는 표이다.

kernel이 process 별로 유지하며 열린 파일에 대한 각종 정보를 관리한다.

Access mode, file offset, pointer to files

 

 

Closing files

#include <unistd.h>

int close(int fd);

- fd(file descriptor)

닫으려는 file descriptor

return: 0(success), -1(error)

file descriptor를 끊어주는 역할을 한다.

 

그럼 open과 close를 이용하여 간단하게 파일을 열고 닫는 프로그램을 만들어보자

이렇게 작성을 하고 컴파일을 해본다면

이렇게 새로운 파일이 생성되는 것을 볼 수 있다.

 

Error handling for System call

System call은 실패 시 -1을 반환

Error code는 errno에 저장 되며

perror를 활용해 error message를 출력 할 수 있다.

#include <stdio.h>

void perror(const char *str);

 

Reading a file

#include <stdio.h>

ssize_t read(int fd, void *buf, size_t count);

-fd(file descriptor)

읽으려는 파일의 file descriptor

-buf(buffer)

읽은 내용을 저장할 buffer의 시작 주소

-count

읽을 byte의 수

return: 실제로 읽은 byte의 수 (0: 파일의 끝인 EOF에 도달, -1: 에러)

 

Writing to a file

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

-fd(file descriptor)

기록하려는 파일의 file descriptor

-buf(buffer)

기록할 내용이 저장된 buffer의 시작 주소

-count

기록할 byte의 수

return: 실제로 기록한 byte의 수 (-1: 에러)

 

File offset(File position)

File operation을 적용할 위치

파일의 시작점부터 현재 위치까지의 byte 수

 

Read/Write 수행시, 읽은/기록한 byte 수 만큼 순차적으로 이동

file offset을 이용해 해당 파일을 어디까지 읽었는지, 어디까지 작성했는지 기억한다.

 

그럼 한 번 Read와 Write까지 활용해서 파일을 작성해보자

#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void){
	int rfd, wfd, n;
	char buf[10];

	rfd = open("hello.txt", O_RDONLY);
	if(rfd == -1){
		perror("Open hello.txt");
		exit(1);
	}

	wfd = open("hello.bak", O_CREAT | O_WRONLY | O_TRUNC, 0644);
	if(wfd == -1){
		perror("Open hello.bak");
		exit(1);
	}

	while((n = read(rfd, buf, 6)) > 0)
		if(write(wfd, buf, n) != n) perror("wrote");

	if(n == -1) perror("Read");
	
	close(rfd);
	close(wfd);

	return 0;
}

이렇게 read를 이용하여 offset을 따라 모두 읽고 write하는 것을 볼 수 있다.

 

우리는 당연히 파일을 처음부터 순차적으로 따라 읽으며 내려왔다.

하지만 파일의 원하는 Block에 직접 접근 하는 방법도 있다.

 

Moving the file offset

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

-fd(file descriptor)

대상 file descriptor

-offset

이동시킬 byte 수 (양수 or 음수)

-whence

기준 위치

return: 이동 후 file offset (-1: 에러)

 

whence에는

SEEK_SET: 파일의 시작

SEEK_CUR: 현재 위치

SEEK_END: 파일의 끝

 

이렇게 3가지 종류가 있다.

 

한 번 사용해보도록 하자

#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void){
    int fd, n;
    off_t start, cur;
    char buf[256];

    fd = open("linux.txt", O_RDONLY);
    if(fd == -1){
        perror("Open linux.txt");
        exit(1);
    }

    start = lseek(fd, 0, SEEK_CUR);
    n = read(fd, buf, 255);
    buf[n] = '\0';
    printf("Offset start=%d, Read Str=%s, n=%d\n", (int)start, buf, n);
    cur = lseek(fd, 0, SEEK_CUR);
    printf("Offset cur=%d\n", (int)cur);

    start = lseek(fd, 6, SEEK_SET);
    n = read(fd, buf, 255);
    buf[n] = '\0';
    printf("Offset start=%d, Read Str=%s", (int)start, buf);

    close(fd);

    return 0;
}

실행을 시키면 이렇게 offset이 이동하는 것을 볼 수 있다.

 

Page cache & write-Back

Page ache

- In-memory store of recenlty accessd dat from an on-disk filesystem

- Disk 접근 시간 절약을 위해 kernel 내부적 기법

 

page write-back

- Page cache에 변경 된 내용을 disk에 반영하는 것

- 반영 시기는 kernel이 결정

 

Synchronizing with disks

#include <unistd.h>

int fsync(int fd);

-fd(file descriptor)

대상 file descriptor

return: 0(success), -1(error)

page write-back을 강제로 수행한다.

 

  • File descriptor

Duplicating FD

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

-oldfd(old file descriptor)

복사하려는 file descriptor

-newfd

새로운 fd 지정

dup()의 경우 할당 가능한 fd 중 가장 작은 값으로 할당

return: oldfd를 복사한 새로운 fd (-1: 에러)

 

한번 실습을 해보자

#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void){
    int fd, fd1;

    fd = open("tmp.aaa", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if(fd == -1){
        perror("Create tmp.aaa");
        exit(1);
    }
    
    close(1);

    fd1 = dup(fd);

    printf("DUP FD = %d\n", fd1);
    printf("Standard Output Redirection\n");
    close(fd);

    return 0;
}

이거를 실행해보면 stdout이 tmp.aaa로 연결이 되어

터미널에 출력이 되지 않고 tmp.aaa에 저장이 되는 것을 볼 수 있다.

 

 

728x90

어셈블러에서 함수를 어떻게 처리하는지 살펴보자

 

  • Supporting Procedures

함수를 호출하는 절차는 다음과 같다

1. 함수의 매개변수들을 넘긴다.

2. memory space에 필요한 공간을 마련하고 그곳으로 제어를 옮긴다.

3. 함수를 실행한다.

4. return value를 저장하고 돌아갈 위치를 찾는다.

5. 원래 지점으로 돌아간다.

 

int main(){ //caller
	if(a == 0)
    	b = f1(g, h);
    else
    	b = f1(k, i);
    return 0;
}

int f1(x, y){ //callee, caller
	a = (x + y) * (x + 2) * f2(y);
    return a;
}

int f2(y){ //callee
	return y / 2;
}

여기서 main 함수가 f1을 호출한 caller가 되고

f1은 main 함수에 의해 호출당한 callee이며, f2를 호출한 caller가 된다.

f2는 f1에 의해 호출당한 callee이다.

 

 

그러면 RISC-V에서는 매개변수와 return value, return address를 어떻게 주고받을까?

 

RISC-V는 관습적으로 정해진 레지스터 번호에 해당하는 레지스터를 함수의 호출에 사용한다.

 

x10~x17: eight parameter registers in which to pass parameters or return values

x10~x17에 해당하는 레지스터를 매개변수를 저장할 때 사용하거나 return value를 저장할 때 사용한다.

 

x1: one return address register

x1은 함수가 return 할 때 돌아갈 지점을 저장한다.

 

또 함수를 다루기 위해서 jal이라는 명령어를 사용한다.

jal: "jump-and-link"

jal x1, ProcedureAddress

Branch unconditionally and save the address of the next instruction(return address) to the designated register

무조건 지정해 둔 주소로 분기를 하며, 레지스터에 다음번 실행해야 할 instruction을 저장하고 분기한다.

보통 return address를 저장하는 x1 레지스터를 사용한다.

 

만약 돌아올 주소를 저장하지 않고 분기하고 싶다면

jal x0, Label

로 주소를 저장하지 않고 분기할 수 있다.

 

jal에서 더 확장하여 jalr 명령어가 있는데

jalr: "jump and link register" instruction (I-type)

jalr: x0, 0(x1)

x0 주소에 return address를 저장하고 0(x1) 주소로 분기한다는 명령어이다.

 

주소값을 표현하는 데에 jal에 비해 더 많은 비트를 사용하기 때문에 더 자세한 주소값으로 분기할 수 있다.

 

  • Using More Registers

함수를 실행하기 위해서는 보통 8개의 레지스터보다 더 많은 메모리를 필요로 한다.

그렇기에 컴파일러는 레지스터 이외에 추가로 메모리를 사용한다.(당연히 속도는 더 느려진다)

이렇게 레지스터가 모두 차서 메모리에 저장하는 현상을 spill이라고 한다.

 

spill이 일어났을 때 저장하기에 이상적인 공간은 stack이다.

x2 레지스터를 stack pointer라고 하는데

이 stack pointer는 데이터가 저장되어 있는 memory의 주소이다.

이 stack은 높은 주소부터 낮은 주소로 데이터를 저장하기 시작한다.

 

 

stack pointer를 활용하는 예제이다.

	long long int
    left_example(
    	long long int g,
        long long int h,
        long long int i,
        long long int j)
{
	long long int f;
    f = (g + h) - (i + j);
    return f;
}

//g -> x10
//h -> x11
//i -> x12
//j -> x13
//f -> x20

이 C 코드를

leaf_example:
addi sp, sp, -24 //adjust stack to make room for 3 items
sd x5, 16(sp)    //save register x5 for use afterwards
sd x6, 8(sp)     //save register x6 for use afterwards
sd x20, 0(sp)    //save register x20 for use afterwards
add x5, x10, x11 //register x5 contains g + h
add x6, x12, x12 //register x6 contains i + j
sub x20, x5, x6
addi x10, x20, 0 //move value f to x10
ld x20, 0(sp)    //restore register x20 for caller
ld x6, 8(sp)     //restore register x6 for caller
ld x5, 16(sp)    //restore register x5 for caller
addi sp, sp, 24  //adjust stack to delete 3 items
jalr x0, 0(x1)

x5, x6, x20에 있는 값을 sp를 이동시킨 후 memory에 저장하는 모습이다.

사용 후에는 caller의 데이터를 복원하기 위해 레지스터에 원래 데이터를 두고 sp를 원위치시킨다.

stack을 사용하던 도중 저 sp는 처음 위치보다 더 올라가서 읽으면 안된다.

그렇기에 시작 주소를 fp에 저장하고 그 위로는 데이터를 읽을 수 없도록 한다.

sp는 함수 실행 도중 변할 수 있지만 fp는 그렇지 못하다.

 

 

  • Register saving convention

하지만 모든 상황에서 caller의 데이터를 복구해 두는 것은 아니다.

No need for callee to save callee must preserve if used
x5-x7, x28-x31 x8, x9, x18-x27

저 표에 있듯 x8, x9, x18-x27의 데이터는 복구를 해두어야 하지만 x5-x7, x28-x31의 값은 복구할 필요가 없다.

다시 말해 caller는 callee를 호출 한 후 x5-x7, x28-x31의 값이 원래와 같지 않을 수 있다고 생각을 해야한다는 것이다.

 

저 표에 sp, fp 등의 레지스터를 추가해보면

Preserved Not preserved
Saved registers: x8-x9, x18-x27 Temporary registers: x5-x7, x28-x31
Stack pointer register: x2(sp) Argument/result registers: x10-x17
Frame Pointer: x8(fp)  
Return address: x1(ra)  

다음과 같다.

 

  • Allocating Memory on the Heap

프로그램 실행 중에 데이터를 메모리에 저장하는 방법이다.

Text Segment: instructions, program binaries

Static data segment: constants and static variables

Heap(for dynamic data): place where dynamically allocated(malloc and free in C)

stack은 위에 나왔던 stack 과 같다.

 

처음 실행 할 때 Text, Static data 영역에 데이터를 저장해두고 프로그램 실행 중에 Heap 영역만 변하게 된다.

 

  • Instructions

lui

immediate에 저장되어 있는 12비트의 수를 64비트로 확장하는 instruction

우선 31~12에 저장되어 있는 20비트의 수를 load한 후

저장하려는 64비트의 왼쪽에 32비트를 sign 비트로 채운다.

그 뒤에 20비트의 수를 넣고 나머지 오른쪽의 12비트를 0으로 채운다.

 

SB-type

beq, bne등의 instruction이 해당하며 분기하는 instruction이다.

immediate를 이용해서 -4096 ~ 4094 범위로 분기 할 수 있다.

imm[0]은 언제나 0이기 때문에 따로 저장하지 않는다.(항상 짝수이기 때문)

 

UJ-type

조건에 상관없이 무조건 분기하는 jal 계열의 instruction이다.

조건을 확인하지 않기에 immediate가 더 긴 것을 볼 수 있으며, 여기도 imm[0]이 항상 짝수이기 때문에 따로 저장하지 않는다.

 

여기서 보면 conditional branch는 13bit, unconditional branch는 21bit의 immediate field를 가진다.

부호를 표현하는 데 1bit, 4byte instruction이기에 2bit가 빠지면

conditional branch는 2의 10제곱 만큼의 위치를

unconditional branch는 2의 18제곱 만큼의 위치로 분기 할 수 있다.

'학교 생활 > 컴퓨터구조' 카테고리의 다른 글

컴퓨터구조 3주차  (0) 2023.01.13
728x90

10일 정도 안에 마칠 계획이고

보통은 WSL Ubuntu를 사용하겠지만 MAC을 가지고 있기 때문에 MAC을 이용해서 시스템 프로그래밍을 정리할 예정이다.

 

  • 시스템 프로그래밍의 개요

Systems programming, or system programming, is the activity of programming computer system software. The primary distinguishing characteristic of systems programming when compared to application programming is that application programming aims to produce software which provides services to the user directly (e.g. word processor), whereas systems programming aims to produce software and software platforms which provide services to other software, are performance constrained, or both - from wikipedia

 

시스템 프로그래밍은 시스템 호출을 이용해서 프로그램을 작성하는 것이다.

시스템 호출은 운영체제에서 제공하는 서비스를 프로그램에서 이용할 수 있도록 지원하는 프로그래밍 인터페이스를 의미한다.

 

시스템 프로그램은 시스템을 구성하는 자원을 다루는 프로그램이며 응용프로그램에 서비스를 제공하는 프로그램으로 OS, firmware, compiler 등이 있다.

 

프로그래밍을 하면서 라이브러리는 많이 사용을 해보았을 것이다.

Library function과 System call을 비교해보면

System call: 시스템의 기능을 이용하는 통로, Kernel 내 모듈을 직접 호출

Library function: 미리 컴파일 된 함수, 자주 사용되는 기능을 독립적으로 구현 (내부적으로 System call을 활용)

 

미리 만들어진 Library를 사용할 수 있지만 굳이 System programming을 배워 사용하는 이유는

직접 System call을 사용한다면 더 많은 것들을 만들 수 있으며 중간 과정을 건너 뛰고 직접 사용한다면 더 높은 성능을 얻을 수 있기 때문이다.

 

이 시스템 프로그래밍 공부의 목표는

  • 시스템을 효율적으로 활용하는 능력 함양

Unix/Linux 기반 시스템 및 새로 만나는 시스템

 

  • 시스템 자원에 대한 이해 향상

- File / Directory

- System information

- Process

- Signal

- Memory

- Communication interface

 

그냥 프로그램을 만드는 것이 아니라 더 높은 성능의 프로그램을 만들 수 있도록 시스템에 대해 이해하는 것이다.

 

  • Unix/Linux architecture

시스템 프로그래밍 중 사용할 Linux/Unix에 대해 알아보자

현재, 다양한 시스템에서 가장 널리 사용하는 OS이다.

 

Unix/Linux architecture

 

- Kernel

OS의 핵심 기능을 모아놓은것

 

-System call

OS 기능 중 응용프로그램이 사용하도록 허락된 기능들의 집합

 

-Shell

사용자와 OS 사이의 인터페이스

사용자의 입력을 해석하여 커널의 함수 호출

 

  • Basic Unix/Linux commands

- Login/Logout

telnet: 유닉스 시스템에 접속

이거는 잘 안써서 사용 방법을 모르겠다....

logout: 유닉스 시스템에서 접속해제

ex) logout

exit: 유닉스 시스템에서 접속해제

ex) exit

 

-Process management

ps: 현재 실생중인 프로세스를 출력

ex) ps -u 사용자ID

kill: process 강제 종료

ex) kill -9 사용자ID

 

-File/Directory

pwd: 현재 디렉토리 경로 출력

ex) pwd

ls: 디렉토리 내용 출력

ex) ls -l

cd: 현재 디렉토리 변경

ex) cd /tmp

cp: 파일/디렉토리 복사

ex) cp a.txt b.txt

mv: 파일/디렉토리 이름변경과 이동

ex) mv a.txt b.txt

rm: 파일/디렉토리 삭제

ex) rm a.txt

mkdir: 디렉토리 생성

ex) mkdir dir1

rmdir: 빈 디렉토리 삭제

ex) rmdir dir2

cat: 파일 내용 출력

ex) cat a.txt

more: 파일 내용을 쪽 단위로 출력

ex) more a.txt

chmod: 파일 접근권한 변경

ex) chmod 777 a.txt

grep: 패턴 검색

ex)grep abcd a.txt

 

- Others

su: 사용자 계정 변경

ex) su -seungkyu

tar: 파일/디렉토리 묶기

ex) tar cvf a.tar *

whereis: 파일 위치 검색

 

Man Page

Unix/Linux 시스템 사용 설명서로 

Shell command, System Call, Standard library에 대한 설명을 제공

 

man [options][section] page

의 명령어를 입력하여 사용한다.

 

  • VI editor

Linux를 사용하다보면 VI editor를 굉장히 많이 사용할 것이다.

일단 배운 명령어들로 실습을 할 디렉토리를 만들고 들어가준다.

그럼 이 실습 디렉토리에서 vi helloLinux.c 라는 명령어를 입력하여 VI 에디터를 실행해보자

VI 에디터로 helloLinux.c라는 C 소스코드 파일을 작성하겠다는 명령어이다.

 

그러면

이런 창이 나타나게 된다.

윈도우에서의 메모장이라고 생각하면 된다.

 

VI Editor의 명령어들이다.

입력모드 전환: i, a, o, O

한글자 수정: r

단어수정: cw

붙이기: p

글자삭제: x

명령취소: u, U

복사: y

행삭제: d

저장하고 종료: wq!

저장하지 않고 종료: :q!

 

명령어들은 외우려고 하기 보다 쓰다보면 자연스럽게 익혀지기 때문에 많이 사용해보는 것이 좋다.

정리하는 단계이니 일일이 소개하지는 않고

 

바로 프로그램을 만들어보도록 하겠다.

이렇게 작성을 하고 이 코드를 컴파일해보자

gcc -o helloLinux helloLinux.c

gcc를 이용해서 컴파일을 한다.

이렇게 입력을 하면 컴파일이 되고

./helloLinux를 이용해 컴파일이 된 프로그램을 실행할 수 있다.

 

 

  • Makefile

GNU C compiler는 Unix/Linux에서 가장 기본이 되는 compiler이다.

위에서 대충 사용해봤겠지만 자세한 사용 방법은 아래와 같다.

 

gcc [options] filename

options:

-c: object file(.o)만 생성

-o: execution file name 지정(default는 a.out이고 사용자가 이름 지정가능)

이렇게 -c 옵션을 주면 .o 파일을 만들 수도 있다.

Makefile & Make

컴파일을 도와주는 Make에 대해 알아보자

Makefile은 compile 방법을 기술 하는 파일이다.(관련 파일 정보, compile 명령, 실행 파일명 등), 여러 파일로 구성된 프로젝트 설정과 비슷한 개념

 

Make은 주어진 Makefile에 따라 compile을 수행하고, 실행파일을 생성. 최초 컴파일 이후에는, 변경이 있는 파일만 컴파일 함

 

Makefile을 사용하면

$ gcc -c -o main.o main.c
$ gcc -c -o foo.o foo.c
$ gcc -c -o bar.o bar.c

$ gcc -o app.out main.o foo.o bar.o

이렇게 항상 하나하나 모두 직접 컴파일을 해줘야 했던 일을

 

이렇게 미리 Makefile에 작성하여

app.out: main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o
    
main.o: foo.h bar.h main.c
	gcc -c -o main.o main.c
    
foo.o: foo.h foo.c
	gcc -c -o foo.o foo.c
    
bar.o: bar.h bar.c
	gcc -c -o bar.o bar.c

$ make 명령어로 한 번에 컴파일 할 수 있다.

이렇게 미리 작성을 해두면 오타날 위험이 적다는 장점도 있다.

그리고 make를 할 때마다 모든 파일들을 컴파일 하는 것이 아니라 수정된 파일들만 찾아서 컴파일을 해주게 된다.

 

Rule block

Makefile의 Rule block은

<Target>: <Dependencies>
	<Recipe>

Target: Build 대상 이름, 일반적으로 최종 결과 파일명 사용

Dependency: Build 대상이 의존하는 Target이나 파일 목록

Recipe: Build 대상을 생성하는 명령어

 

Implicit rules

자주 사용되는 규칙을 자동으로 처리해줌(Source를 Compile해서 Object 파일을 생성하는 규칙)

Target에 대한 dependency까지는 명시를 해주어야 한다.

 

app.out: main.o foo.o bar.o
	gcc -o app.out main.o foo.o bar.o
    
main.o: foo.h bar.h main.c
foo.o: foo.h foo.c
bar.o: bar.h bar.c

이렇게 아래부분을 더 짧게 바꿀 수 있다.

 

variables

사용하는 명령어들과 파일 명들을 변수로 치환해서 사용할 수 있다.

CC=gcc
CFLAGS=-g pWall
OBJS=main.o foo.o bar.o
TARGET=app.out

$(TARGET): $(OBJS)
	$(CC) -o $@ $(OBJS) // $@: current target name
    
main.o: foo.h bar.h main.c
foo.o: foo.h foo.c
bar.o: bar.h bar.c

 

Clean rule

Build로 생성된 파일들 삭제하는 규칙

 

make로 파일을 만든 후에 필요 없어지는 파일들이 생길 수 있다.

그럴 때도 make 파일에 clean을 작성 한 후 한번에 제거할 수 있다.

clean:
    rm -f *.o
    rm -f $(TARGET)

이렇게 작성을 해둔 후

$make clean 명령어를 입력하면 clean에 작성된 명령어들이 한 번에 실행이 된다.

 

CC=gcc # (built-in variable) compiling C programs
CFLAGS=-g pWall # (built-in variable) extra flags to give to CC
OBJS=main.o foo.o bar.o
TARGET=app.out

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) -o $@ $(OBJS) // $@: current target name
    
main.o: foo.h bar.h main.c
foo.o: foo.h foo.c
bar.o: bar.h bar.c

clean:
    rm -f *.o
    rm -f $(TARGET)

이렇게 배워본 makefile을 정리해 보았다.

728x90

컴퓨터구조 3주차 정리이다.

 

전에 다 알아보지 못했던 2의 보수법과 CPU의 기본 명령어들을 알아보도록 하자

 

  • Signed and Unsigned Numbers

컴퓨터는 단순 2진수로만 수를 저장할 수 있지만 양수만이 아닌 음수도 저장할 수 있다.

그렇다면 과연 컴퓨터 내에서는 음수를 어떻게 저장할까?

 

수를 저장할 때 부호 비트의 자리를 따로 만들고 해당 비트가 0이면 양수, 1이면 음수로 표현한다.

부호 비트를 앞쪽에 저장하는지, 뒤쪽에 저장하는지에 따라서 LSB와 MSB로 나뉘는데

 

LSB(least significant bit): the right most bit, bit position 0

부호 비트를 0번에 표시하면 LSB

 

MSB(most significant bit): the left most bit, bit position 63

부호 비트를 63번에 표시하면 MSB

이다.

 

보통은 MSB를 사용한다.

 


 

  • signed numbers

음수를 부호 비트 1로 표현하는 것은 알았다.

그렇다면 그냥 단순히 부호비트만 1로 바꾸면 음수가 되는 것일까?

 

0000 = 0      -> 1000 = -0

0001 = 1       -> 1001 = -1

0010 = 2       -> 1010 = -2... 이런식으로 양수에서 부호비트만 바꾸는 방법을 sign and magnitude(부호-크기)방식이라고 한다.

하지만 해당 방식의 문제는 뺄셈의 계산을 할 때 발생한다.

뺄셈은 음수의 더하기로 계산을 한다.

그렇다면 이 식을 계산해보자

 

6 - 3 = 0110 + 1011 = ..0001 대충 계산을 해보아도 다른 답이 나오는 것을 볼 수 있다.

 

뺄셈을 위해 컴퓨터에서는 이 부호-크기 방식을 사용하지 않고 보수법을 사용하게 된다.

 

1's complement(1의 보수법)

0000 = 0      -> 1111 = -0

0001 = 1       -> 1110 = -1

0010 = 2       -> 1101 = -2 ... 이런식으로 양수와 음수 비트를 역전시키는 방법을 1's complement(1의 보수법)이라고 한다.

이 방법을 사용하면 뺄셈을 수행할 수 있다.

 

6 - 3 = 0110 + 1100 = 0010 = 2, 비록 3이 나오지는 않지만 이 수에 1을 더해주면 결과가 나오게 된다.

뺄셈 후에는 모두 일관되게 1만 더해주면 되기 때문에 뺄셈 연산이 가능하다.

 

하지만 이 방법의 단점은 무엇일까?

0을 표현하는 방법이 2가지 이다.(0000, 1111)

이렇게 된다면 표현할 수 있는 수가 하나 줄게 된다.

 

2's complement(2의 보수법)

0000 = 0      -> 1111 = -1

0001 = 1       -> 1110 = -2

0010 = 2       -> 1101 = -3 ... 이런식으로 1의 보수법에서 아예 미리 1을 더해놓는 방법을 2's complement(2의 보수법)이라고 한다.

이 방법을 사용하면

 

6 - 3 = 0110 + 1101 = 0011 = 3, 이렇게 바로 원하는 결과를 얻을 수 있으며

0이 중복으로 표현되지 않기 때문에 표현할 수 있는 수가 손실되지 않는다.

 

양수와 음수를 변환 할 때, 비트를 역전시킨 후에 1만 더해주면 된다.

2 -> -2 는 0010 -> 1101 -> add1 -> 1110으로

-2 -> 2 는 1110 -> 0001 -> add2 -> 0010으로 상호변환이 가능하다.

이렇게 양수와 음수를 표현하는 방법에 대해 알아보았다.

그렇다면 sign extension(부호 확장)에 대해 알아보자

 

sign extension

이번엔 부호의 확장에 대해 알아보자.

n bit였던 수를 m bit로 확장한다면

해당 수의 부호를 보존하기 위해 MSB 비트를 확장하는 만큼 앞에 채워주면 된다.

그래도 만약 원한다면 0으로 채워줄 수는 있다.

 

Instructions : lb(load byte), lh(load halfword), lw(load word)

if you don't want to sign extension, use lbu(load byte unsigned).

 


 

  • Representing instructions

컴퓨터에서 수를 어떻게 저장하는지 살펴보았으니 이제 명령어들을 어떻게 표현하는지 알아보자.

가장 대표적인 명령어 타입으로 R-type, I-type, S-type 이 있다.

 

R-type

R-type 명령어는 이렇게 이루어져 있다.

opcode: 대략적인 명령어의 종류

rd: destination register(값을 저장할 레지스터)

funct3: additional opcode

rs1: 1st source register

rs2: 2nd source register

funct7: additional opcode 

 

x20과 x21을 더해서 x9에 저장하는 add x9, x20, x21을 표현한다면

0000000/10101/10100/000/01001/0110011로 표현이 될 것이다.

 

I-type

i가 붙은 명령어들을 수행하는 I-type instruction이며

이렇게 표현이 된다.

R-type에서 rs2와 funct7 코드가 합쳐져 immediate가 되었으며 immediate는 연산을 수행 할 때 같이 계산이 되는 상수이다.

 

x2의 레지스터에 있는 주소값에서 8을 더한 주소의 값을 x14에 저장하는 ld x14, 8(x2)를 표현한다면

000000001000/00010/011/01111/0000011로 표현이 된다.

 

S-type

S-type에는 rd가 없는것이 특징이다. (레지스터에 데이터를 저장하는 과정이 필요하지 않기 때문)

대신 immediate가 rd의 위치까지 차지한 상태로 나뉘어 저장이 된다.

x2의 레지스터에 있는 주소값에서 8을 더한 주소의 값을 x14에 저장되어 있는 주소에 저장하는 sd x14, 8(x2)를 표현한다면

0000000/01110/00010/011/01000/0100011로 표현이 된다.

 

  • logical operations

논리적인 계산을 수행하는 명령어들을 알아보자.

Shift left와 right 부터 살펴보자.

왼쪽으로 이동하는 Shift left는 그냥 0으로 채우지만, 오른쪽으로 이동하는 Shift right는 무슨 수로 채우는지에 따라 부호가 바뀌기 때문에 신중해야 한다.

 

slli, srli - shift left (right) logical immediate

이 명령어들은 단순히 이동시킨 후 빈자리를 0으로 채워준다.

 

만약 이 문제를 해결하고 싶다면

sra, srai - shift right arithmetic(immediate): fill with sign-bit

sign bit로 채워주는 sra 계열의 명령어를 사용해야한다.

 

AND

OR

XOR

해당 명령어들은 컴퓨터를 공부했다면 직관적으로 파악할 수 있을것이다.

 

여기에 NOT연산은 따로 구현해두지 않고

1111...1111과 xor연산으로 구할 수 있다.

 


 

  • Instructions for Making Decisions

컴퓨터가 결정을 내리는 과정 코드에 대해 알아보자

쉽게 IF문으로 생각하면 된다.

 

컴퓨터는 Branch instruction을 이용해 분기를 한다.

 

Branch instruction

 

- if statement

  • beq rs1, rs2, L1

beq: branch if equal

만약 rs1의 값과 rs2의 값이 같다면 L1으로 분기한다는 조건문이다.

 

  • bne rs1, rs2, L1

bne: branch if not equal

만약 rs1의 값과 rs2의 값이 같지 않다면 L1으로 분기한다는 조건문이다.

 

bne와 beq를 이용해서 C의 코드를 변환해보자

만약 C의 이런 코드가 있다.

그리고 각각의 변수는 해당 레지스터에 값이 저장되어 있다.

f: x19, g:x20, h:x21, i:x22, j:x23

이렇게 변환이 될 것이다.

 

While문도 이렇게 조건을 이용해서 만족할 때만 탈출할 수 있도록 만든다.

i: x22, k: x24, base of save: x25

 

이렇게 계속 만족하는 조건에만 탈출하고 아니면 계속 위로 올라가는 방식으로 조건문을 구현한다.

 

- Other conditional Branches

  • blt: branch is less than

blt rs1, rs2, L1

뜻 그대로 rs1에 저장된 값이 rs2보다 작으면 L1으로 분기한다.

 

  • bge: branch if greater than or equal

bge rs1, rs2, L1

rs1에 저장된 값이 rs2보다 크거나 같으면 분기한다.

 

bltu, bgeu는 비교하는 값들이 unsigned 수라고 생각하고 비교하는 것이다.

 

이것들을 이용해 Array의 범위 체크를 구하는 명령어를 만들어보자

우리는 배열을 사용할 때 해당 인덱스가 0 이상 최대 인덱스 미만인지 확인을 해야한다.

 

당연히 조건문을 두번 사용해야 할 것 같지만

이렇게 bgeu를 한번만 사용한다.

만약 x20에 저장된 수가 음수라면 unsigned 수로는 엄청나게 큰 수로 읽게 되고 그럼 x11보다 클 것이기 때문에

저 식 한번으로도 체크할 수 있다.

'학교 생활 > 컴퓨터구조' 카테고리의 다른 글

컴퓨터구조 4주차  (0) 2023.01.15

+ Recent posts